azurpaint 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
azurpaint/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ __version__ = '1.0.0'
2
+
3
+ from .main import Azurpaint
4
+ from .exception import *
5
+ from .types import *
6
+ from .lib import Inspector, Tree, Node
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from .Vector2 import Vector2
4
+
5
+
6
+ class AABB:
7
+ center: Vector2
8
+ extend: Vector2
9
+
10
+ def __init__(self, center: Vector2, extend: Vector2) -> None:
11
+ self.center = center
12
+ self.extend = extend
13
+
14
+
15
+ def __repr__(self) -> str:
16
+ return f"<{self.__class__.__name__} center={self.center.values()} extend={self.extend.values()}>"
17
+
18
+
19
+ @property
20
+ def padding(self) -> Vector2:
21
+ return self.center - self.extend
22
+
23
+
24
+ @property
25
+ def size(self) -> Vector2:
26
+ return self.center + self.extend
@@ -0,0 +1,259 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union, cast
4
+ from pathlib import Path
5
+ from UnityPy import Environment, classes
6
+ from UnityPy.enums import ClassIDType
7
+ from UnityPy.files import ObjectReader
8
+
9
+ from ..types import PathLike
10
+ from ..exception import PrefabNotFound
11
+
12
+
13
+ class AssetReader:
14
+ """
15
+ Read UnityAsset using UnityPy.
16
+ """
17
+ path: Path
18
+ prefab: Path
19
+ environment: Environment
20
+ files: List[str] = []
21
+ face: Dict[str, int]
22
+
23
+ def __init__(self, path: PathLike, prefab: PathLike) -> None:
24
+ self.face = {}
25
+ self.path = Path(path)
26
+ self.prefab = Path(prefab)
27
+
28
+ if not self.filepath.exists():
29
+ raise FileNotFoundError(f"File {self.filepath.as_posix()!r} not exists.")
30
+
31
+ self.environment = Environment(self.filepath.as_posix())
32
+ self.files.append(self.prefab.as_posix())
33
+
34
+ if not self.has_prefab:
35
+ raise PrefabNotFound(f"Prefab {self.prefab.as_posix()!r} not exists.")
36
+
37
+
38
+ def __repr__(self) -> str:
39
+ return f"<{self.__class__.__name__} prefab={self.prefab.as_posix()}>"
40
+
41
+
42
+ @property
43
+ def root(self) -> classes.GameObject:
44
+ return self.get_object_by_path_id(next(iter(self.environment.container.values())).path_id)
45
+
46
+
47
+ @property
48
+ def filepath(self) -> Path:
49
+ return Path(self.path, self.prefab)
50
+
51
+
52
+ @property
53
+ def has_prefab(self) -> bool:
54
+ try:
55
+ return any(key.endswith('.prefab') for key in self.environment.container.keys())
56
+ except:
57
+ return False
58
+
59
+
60
+ @property
61
+ def cabs(self) -> List[str]:
62
+ return list(k for k in self.environment.cabs.keys() if not k.endswith('.ress'))
63
+
64
+
65
+ @property
66
+ def dependencies(self) -> List[str]:
67
+ assets: classes.AssetBundle = self.get_object_by_path_id(1)
68
+
69
+ if not hasattr(assets, 'm_Dependencies'):
70
+ raise ValueError(f"Prefab {self.prefab.as_posix()} is missing 'AssetBundle.m_Dependencies'.")
71
+
72
+ return cast(List[str], assets.m_Dependencies)
73
+
74
+
75
+ def get_cabs(self, name: PathLike) -> List[str]:
76
+ environment = Environment(Path(name).as_posix())
77
+ return list(k for k in environment.cabs.keys() if not k.endswith('.ress'))
78
+
79
+
80
+ def load(self, path: PathLike, is_face: bool = False) -> Path:
81
+ path = Path(path)
82
+
83
+ if not path.is_relative_to(self.path):
84
+ path = Path(self.path, path)
85
+
86
+ # is_dependencies is not used, it's a bit complicated with azurlane assets
87
+ # since some asset like paintingface is not an direct dependency for a lot ship
88
+ self.environment.load_file(path.as_posix())
89
+ self.files.append(path.relative_to(self.path).as_posix())
90
+
91
+ if is_face or path.is_relative_to(Path(self.path, 'paintingface')):
92
+ self.face = self._get_face(path)
93
+
94
+ return path
95
+
96
+
97
+ def loads(self, path: Union[PathLike, Iterable[PathLike]]) -> List[Path]:
98
+ if not isinstance(path, Iterable) or isinstance(path, str):
99
+ path = [path]
100
+
101
+ _loaded = []
102
+
103
+ for link in path:
104
+ _loaded.append(self.load(link))
105
+
106
+ return _loaded
107
+
108
+
109
+ def _gather_files(self, folders: List[str] = ['painting', 'paintings', 'paintingface']) -> List[str]:
110
+ """
111
+ Get all files list that have same name as prefab in painting, paintings and paintingface
112
+ """
113
+ files: List[str] = []
114
+
115
+ for folder in folders:
116
+ glob = Path(self.path, folder).glob(f"{self.prefab.name.split('_')[0]}*")
117
+ files.extend([file.relative_to(self.path).as_posix() for file in glob])
118
+
119
+ return files
120
+
121
+
122
+ def _get_face(self, path: Path) -> Dict[str, int]:
123
+ face = {}
124
+
125
+ for obj in cast(List[ObjectReader], self.environment.files[path.as_posix()].get_objects()):
126
+ if obj.type != ClassIDType.Sprite:
127
+ continue
128
+
129
+ obj = cast(classes.Sprite, obj.read())
130
+ face[obj.name] = obj.path_id
131
+
132
+ return {key: face[key] for key in sorted(face.keys())}
133
+
134
+
135
+
136
+ def _load_face(self, force: bool = True) -> None:
137
+ if len(self.face):
138
+ return
139
+
140
+ files = self._gather_files(['paintingface'])
141
+ prefab_face = Path('paintingface', self.prefab.name).as_posix()
142
+
143
+ if prefab_face in files:
144
+ self.load(prefab_face, is_face=True)
145
+ return
146
+
147
+ if not force:
148
+ return
149
+
150
+ # face not found or not same as prefab name
151
+ # sometimes face have different name with underscore extension
152
+ # but still have same basename eg like _hx extension
153
+ # this is not perfect it may load wrong face
154
+ possible_name: List[str] = [prefab_face]
155
+
156
+ while '_' in prefab_face:
157
+ prefab_face = prefab_face.rsplit('_', 1)[0]
158
+ possible_name.append(prefab_face)
159
+
160
+ matches: List[str] = [file for file in files if file in possible_name]
161
+
162
+ if len(matches) == 0:
163
+ return
164
+
165
+ # when matches is more than one. probably the longest is more desired one
166
+ # since it splitted with underscore so we're prefer longer string
167
+ self.load(max(matches, key=len), is_face=True)
168
+
169
+
170
+ def find_dependencies(self) -> Tuple[Set[str], Set[str]]:
171
+ """
172
+ Find all dependencies in loaded path
173
+
174
+ :returns: tuple[`result`, `missing_dependencies`]
175
+ """
176
+ result: Set[str] = set()
177
+ dependencies: Set[str] = set(self.dependencies)
178
+
179
+ if not len(dependencies):
180
+ return result, dependencies
181
+
182
+ files: List[str] = self._gather_files()
183
+
184
+ # blank image used by asset to create canvas
185
+ files.append('painting/touming_tex')
186
+
187
+ for file in files:
188
+ if not len(dependencies):
189
+ break
190
+
191
+ for cab in self.get_cabs(Path(self.path, file)):
192
+ if cab in dependencies:
193
+ result.add(file)
194
+ dependencies.remove(cab)
195
+
196
+ return result, dependencies
197
+
198
+
199
+ def load_dependencies(self, force_face_load: bool = False) -> None:
200
+ """
201
+ Automatically search for dependencies by name.
202
+
203
+ :params force_face_load: force load face with matching name
204
+
205
+ Warning
206
+ -------
207
+ This is still in experimental state, undefined behaviour may occurs.
208
+ """
209
+ dependencies, _ = self.find_dependencies()
210
+
211
+ if not len(self.face):
212
+ self._load_face(force=force_face_load)
213
+
214
+ self.loads(dependencies)
215
+
216
+
217
+ def get_object_by_path_id(self, path_id: int) -> Any:
218
+ for obj in self.environment.objects:
219
+ if obj.path_id == path_id:
220
+ return cast(Any, obj.read(return_typetree_on_error=False))
221
+
222
+
223
+ def get_object_by_name(self, name: str, type: ClassIDType) -> Any:
224
+ for obj in self.environment.objects:
225
+ try:
226
+ if not hasattr(obj, 'name') or (obj.type != type):
227
+ continue
228
+
229
+ obj = cast(Any, obj.read(return_typetree_on_error=False))
230
+
231
+ if hasattr(obj, 'name') and (str(obj.name) == str(name)):
232
+ return obj
233
+
234
+ except:
235
+ continue
236
+
237
+
238
+ def get_component_from_object(
239
+ self,
240
+ gameobject: classes.GameObject,
241
+ types: Optional[Iterable[ClassIDType]] = None,
242
+ names: Optional[Set[str]] = None,
243
+ attributes: Optional[Set[str]] = None
244
+ ) -> Any:
245
+ for component_pptr in cast(List[classes.PPtr], gameobject.m_Components):
246
+ if types and (not component_pptr.type in types):
247
+ continue
248
+
249
+ component = self.get_object_by_path_id(component_pptr.path_id)
250
+
251
+ if names and (not component.name in names):
252
+ continue
253
+
254
+ if attributes:
255
+ for attribute in attributes:
256
+ if hasattr(component, attribute):
257
+ return component
258
+
259
+ return component
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ from PIL import Image
4
+ from typing import Generator, List, Optional, Union, cast
5
+ from UnityPy import classes
6
+ from UnityPy.enums import ClassIDType
7
+
8
+ from ..exception import \
9
+ MeshImageNotFound, MonoBehaviourNotFound, RectTransformNotFound, TransformNotFound
10
+
11
+ from .AssetReader import AssetReader
12
+ from .MeshImage import MeshImage
13
+ from .RectTransform import RectTransform
14
+ from .Vector2 import Vector2
15
+
16
+
17
+
18
+ class GameObject:
19
+ reader: AssetReader
20
+
21
+ name: str
22
+ path_id: int
23
+ active: bool
24
+ parent: Optional[GameObject]
25
+ children: List[GameObject]
26
+
27
+ # Unity component
28
+ Transform: Union[classes.Transform, classes.RectTransform]
29
+ RectTransform: Optional[RectTransform]
30
+ MonoBehaviour: Optional[MeshImage]
31
+
32
+ # variable
33
+ scale: Vector2
34
+ size: Vector2
35
+ local_offset: Vector2
36
+ global_offset: Vector2
37
+ image: Optional[Image.Image]
38
+
39
+
40
+ def __init__(
41
+ self,
42
+ reader: AssetReader,
43
+ gameobject: classes.GameObject,
44
+ parent: Optional[GameObject] = None
45
+ ) -> None:
46
+ self.reader = reader
47
+
48
+ self.name = gameobject.name
49
+ self.path_id = gameobject.path_id
50
+ self.active = bool(gameobject.m_IsActive)
51
+ self.parent = parent
52
+ self.children = []
53
+
54
+ # default value to prevent attr not found
55
+ self.RectTransform = None
56
+ self.MonoBehaviour = None
57
+ self.image = None
58
+ self.scale = Vector2.one()
59
+ self.size = Vector2.zero()
60
+ self.local_offset = Vector2.zero()
61
+ self.global_offset = Vector2.zero()
62
+
63
+
64
+ # this can be improved but im too lazy to change this
65
+ self.Transform = cast(Union[classes.RectTransform, classes.Transform],
66
+ self.reader.get_component_from_object(
67
+ gameobject=gameobject,
68
+ types=[ClassIDType.RectTransform, ClassIDType.Transform]
69
+ )
70
+ )
71
+
72
+ if not self.Transform:
73
+ # unexpected behaviour, since every component in unity
74
+ # should have an Transform or RectTransform by default
75
+ raise TransformNotFound(f"Transform not found in {self}.")
76
+
77
+ if self.Transform.type == ClassIDType.RectTransform:
78
+ self.RectTransform = RectTransform(cast(classes.RectTransform, self.Transform))
79
+
80
+ # Transform have scale too
81
+ self.scale = Vector2.from_value(self.Transform.m_LocalScale)
82
+
83
+ if self.parent and not self.parent.is_root:
84
+ self.scale *= self.parent.scale
85
+
86
+ if not (self.is_root or self.active):
87
+ return
88
+
89
+ try:
90
+ self.MonoBehaviour = MeshImage(self.reader, gameobject=gameobject)
91
+
92
+ if self.is_root or self.active:
93
+ self.image = self.MonoBehaviour.image
94
+
95
+ if self.image:
96
+ self.size = Vector2.from_value(self.image.size)
97
+
98
+ except (MeshImageNotFound, MonoBehaviourNotFound):
99
+ pass
100
+
101
+ if not self.image:
102
+ return
103
+
104
+ if not self.RectTransform:
105
+ raise RectTransformNotFound(f"RectTransform not found in {self}.")
106
+
107
+ size_delta = self.RectTransform.size_delta
108
+
109
+ # root always use size_delta when active
110
+ # some root doesn't active like qiye_4 or kelifulan_4 it's gonna stretch the image
111
+ # resize only smaller image than size delta
112
+ # we handle image that larger in size delta in offset
113
+ if (self.is_root and self.active) or size_delta.is_bigger(self.size):
114
+ self.image = self.image.resize(size_delta.as_size(), Image.Resampling.LANCZOS)
115
+ self.size = Vector2.from_value(self.image.size)
116
+ return
117
+
118
+
119
+ def __repr__(self) -> str:
120
+ return f"<{self.__class__.__name__} name={self.name}>"
121
+
122
+
123
+ @property
124
+ def root(self) -> GameObject:
125
+ root = self
126
+
127
+ while self.parent:
128
+ root = self.parent
129
+
130
+ return root
131
+
132
+
133
+ @property
134
+ def is_root(self) -> bool:
135
+ return bool(self.parent == None)
136
+
137
+
138
+ def change_face(self, path_id: int) -> bool:
139
+ if self.name != 'face':
140
+ gameobject = self.root.find_child('face')
141
+
142
+ # face should be available in all ship, even ship without face asset still have face gameobject
143
+ if not gameobject:
144
+ raise Exception(f"ERROR: {self.reader.prefab.as_posix()!r} <GameObject name=face> not found.")
145
+
146
+ return gameobject.change_face(path_id=path_id)
147
+
148
+ sprite: Optional[classes.Sprite] = self.reader.get_object_by_path_id(path_id=path_id)
149
+
150
+ if not sprite:
151
+ return False
152
+
153
+ texture2d: classes.Texture2D = self.reader.get_object_by_path_id(
154
+ path_id=sprite.m_RD.texture.path_id)
155
+
156
+ self.active = True
157
+ self.image = texture2d.image
158
+ self.size = Vector2.from_value(self.image.size)
159
+
160
+ if not self.RectTransform:
161
+ raise RectTransformNotFound(f"RectTransform not found in {self}.")
162
+
163
+ size_delta = self.RectTransform.size_delta
164
+
165
+ if size_delta.is_bigger(self.size):
166
+ self.image = self.image.resize(size_delta.as_size(), Image.Resampling.LANCZOS)
167
+ self.size = Vector2.from_value(self.image.size)
168
+
169
+ return True
170
+
171
+
172
+ def find_child(self, name: str) -> Optional[GameObject]:
173
+ for child in self.children:
174
+ if child.name == name:
175
+ return child
176
+
177
+ from_child = child.find_child(name)
178
+
179
+ if from_child:
180
+ return from_child
181
+
182
+
183
+ def retrieve_children(self, recursive: bool = True) -> bool:
184
+ if not self.Transform:
185
+ return False
186
+
187
+ for child in self.Transform.m_Children:
188
+ child_transform = self.reader.get_object_by_path_id(child.path_id)
189
+ child_object = self.reader.get_object_by_path_id(child_transform.m_GameObject.path_id)
190
+ object_layer = GameObject(reader=self.reader, gameobject=child_object, parent=self)
191
+
192
+ if recursive:
193
+ object_layer.retrieve_children(recursive=recursive)
194
+ self.children.append(object_layer)
195
+
196
+ return True
197
+
198
+
199
+ # todo: well move this method into RectTransform?
200
+ def calculate_local_offset(self, recursive: bool = True) -> Vector2:
201
+ if self.RectTransform:
202
+ anchor_min = self.RectTransform.anchor_min
203
+ anchor_max = self.RectTransform.anchor_max
204
+ anchor_pos = self.RectTransform.anchor_pos
205
+ size_delta = self.RectTransform.size_delta
206
+ pivot = self.RectTransform.pivot
207
+
208
+ if anchor_min == anchor_max:
209
+ if self.parent:
210
+ pivot = Vector2(x=pivot.x, y=1 - pivot.y)
211
+
212
+ pivot_offset = (pivot * size_delta) + (0, self.size.y - size_delta.y)
213
+ pivot_anchor = self.scale * pivot_offset
214
+
215
+ anchor_offset = Vector2(x=anchor_pos.x - pivot_anchor.x, y=anchor_pos.y + pivot_anchor.y)
216
+ self.local_offset = (self.parent.size * anchor_min) + (anchor_offset.x, -anchor_offset.y)
217
+
218
+ # resize when local_offset already calculated to prevent miss calculation
219
+ # self.size is the truesize, so size not updated here to make debug easier
220
+ if self.image:
221
+ self.image = self.image.resize(
222
+ (self.size * self.scale).as_size(), Image.Resampling.LANCZOS)
223
+
224
+ else:
225
+ if self.parent:
226
+ self.size = self.parent.size * (anchor_min - anchor_max).apply(abs)
227
+
228
+ self.local_offset = Vector2(x=-anchor_pos.x, y=anchor_pos.y)
229
+
230
+
231
+ if recursive:
232
+ for child in self.children:
233
+ child.calculate_local_offset(recursive=recursive)
234
+
235
+ return self.local_offset
236
+
237
+
238
+ def get_smallest_offset(self) -> Vector2:
239
+ min_offset = Vector2.zero()
240
+
241
+ if self.image:
242
+ min_offset = self.local_offset
243
+
244
+ for child in self.children:
245
+ child_offset = child.get_smallest_offset()
246
+ min_offset = Vector2.min(min_offset, child_offset)
247
+
248
+ return min_offset
249
+
250
+
251
+ def calculate_global_offset(self, offset: Optional[Vector2] = None) -> Vector2:
252
+ offset = offset or self.get_smallest_offset()
253
+
254
+ self.global_offset = self.local_offset - offset
255
+
256
+ for child in self.children:
257
+ child.calculate_global_offset(offset=self.global_offset)
258
+
259
+ return self.global_offset
260
+
261
+
262
+ def get_biggset_size(self) -> Vector2:
263
+ size_offset = Vector2.zero()
264
+
265
+ if self.image:
266
+ # we doesn't want to calculate root scale
267
+ # dev sometimes put root scale into unreasonable value like 90x
268
+ scale = Vector2.one() if self.is_root else self.scale
269
+ size_offset = self.global_offset + (self.size * scale)
270
+
271
+ for child in self.children:
272
+ child_offset = child.get_biggset_size()
273
+ size_offset = Vector2.max(size_offset, child_offset)
274
+
275
+ return size_offset
276
+
277
+
278
+ def yield_layers(self) -> Generator[GameObject, None, None]:
279
+ if self.image:
280
+ yield self
281
+
282
+ for child in self.children:
283
+ for layer in child.yield_layers():
284
+ yield layer