unityflow 0.3.4__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.
- unityflow/__init__.py +167 -0
- unityflow/asset_resolver.py +636 -0
- unityflow/asset_tracker.py +1687 -0
- unityflow/cli.py +2317 -0
- unityflow/data/__init__.py +1 -0
- unityflow/data/class_ids.json +336 -0
- unityflow/diff.py +234 -0
- unityflow/fast_parser.py +676 -0
- unityflow/formats.py +1558 -0
- unityflow/git_utils.py +307 -0
- unityflow/hierarchy.py +1672 -0
- unityflow/merge.py +226 -0
- unityflow/meta_generator.py +1291 -0
- unityflow/normalizer.py +529 -0
- unityflow/parser.py +698 -0
- unityflow/query.py +406 -0
- unityflow/script_parser.py +717 -0
- unityflow/sprite.py +378 -0
- unityflow/validator.py +783 -0
- unityflow-0.3.4.dist-info/METADATA +293 -0
- unityflow-0.3.4.dist-info/RECORD +25 -0
- unityflow-0.3.4.dist-info/WHEEL +5 -0
- unityflow-0.3.4.dist-info/entry_points.txt +2 -0
- unityflow-0.3.4.dist-info/licenses/LICENSE +21 -0
- unityflow-0.3.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
"""Asset Reference Resolver.
|
|
2
|
+
|
|
3
|
+
Provides utilities for resolving asset paths to Unity references with
|
|
4
|
+
automatic GUID and fileID detection.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
# In values, use @ prefix to reference assets
|
|
8
|
+
"@Assets/Scripts/Player.cs" -> Script reference
|
|
9
|
+
"@Assets/Sprites/icon.png" -> Sprite reference (Single mode)
|
|
10
|
+
"@Assets/Sprites/atlas.png:idle_0" -> Sprite sub-sprite (Multiple mode)
|
|
11
|
+
"@Assets/Audio/jump.wav" -> AudioClip reference
|
|
12
|
+
"@Assets/Prefabs/Enemy.prefab" -> Prefab reference
|
|
13
|
+
"@Assets/Materials/Custom.mat" -> Material reference
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from unityflow.asset_tracker import META_GUID_PATTERN
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AssetType(Enum):
|
|
28
|
+
"""Unity asset types for type validation."""
|
|
29
|
+
|
|
30
|
+
SPRITE = "Sprite"
|
|
31
|
+
TEXTURE = "Texture2D"
|
|
32
|
+
AUDIO_CLIP = "AudioClip"
|
|
33
|
+
MATERIAL = "Material"
|
|
34
|
+
PREFAB = "Prefab"
|
|
35
|
+
SCRIPT = "Script"
|
|
36
|
+
SCRIPTABLE_OBJECT = "ScriptableObject"
|
|
37
|
+
ANIMATION_CLIP = "AnimationClip"
|
|
38
|
+
ANIMATOR_CONTROLLER = "AnimatorController"
|
|
39
|
+
FONT = "Font"
|
|
40
|
+
SHADER = "Shader"
|
|
41
|
+
TEXT_ASSET = "TextAsset"
|
|
42
|
+
VIDEO_CLIP = "VideoClip"
|
|
43
|
+
MODEL = "Model"
|
|
44
|
+
UNKNOWN = "Unknown"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Extension to asset type mapping
|
|
48
|
+
EXTENSION_TO_ASSET_TYPE: dict[str, AssetType] = {
|
|
49
|
+
# Sprites/Textures (images are typically used as sprites in 2D)
|
|
50
|
+
".png": AssetType.SPRITE,
|
|
51
|
+
".jpg": AssetType.SPRITE,
|
|
52
|
+
".jpeg": AssetType.SPRITE,
|
|
53
|
+
".tga": AssetType.SPRITE,
|
|
54
|
+
".psd": AssetType.SPRITE,
|
|
55
|
+
".tiff": AssetType.SPRITE,
|
|
56
|
+
".gif": AssetType.SPRITE,
|
|
57
|
+
".bmp": AssetType.SPRITE,
|
|
58
|
+
".exr": AssetType.TEXTURE, # HDR textures
|
|
59
|
+
".hdr": AssetType.TEXTURE,
|
|
60
|
+
# Audio
|
|
61
|
+
".wav": AssetType.AUDIO_CLIP,
|
|
62
|
+
".mp3": AssetType.AUDIO_CLIP,
|
|
63
|
+
".ogg": AssetType.AUDIO_CLIP,
|
|
64
|
+
".aiff": AssetType.AUDIO_CLIP,
|
|
65
|
+
".aif": AssetType.AUDIO_CLIP,
|
|
66
|
+
".flac": AssetType.AUDIO_CLIP,
|
|
67
|
+
# Materials
|
|
68
|
+
".mat": AssetType.MATERIAL,
|
|
69
|
+
# Scripts
|
|
70
|
+
".cs": AssetType.SCRIPT,
|
|
71
|
+
# Prefabs
|
|
72
|
+
".prefab": AssetType.PREFAB,
|
|
73
|
+
# ScriptableObjects
|
|
74
|
+
".asset": AssetType.SCRIPTABLE_OBJECT,
|
|
75
|
+
# Animations
|
|
76
|
+
".anim": AssetType.ANIMATION_CLIP,
|
|
77
|
+
".controller": AssetType.ANIMATOR_CONTROLLER,
|
|
78
|
+
# Fonts
|
|
79
|
+
".ttf": AssetType.FONT,
|
|
80
|
+
".otf": AssetType.FONT,
|
|
81
|
+
".fon": AssetType.FONT,
|
|
82
|
+
# Shaders
|
|
83
|
+
".shader": AssetType.SHADER,
|
|
84
|
+
".shadergraph": AssetType.SHADER,
|
|
85
|
+
# Text/Data
|
|
86
|
+
".txt": AssetType.TEXT_ASSET,
|
|
87
|
+
".json": AssetType.TEXT_ASSET,
|
|
88
|
+
".xml": AssetType.TEXT_ASSET,
|
|
89
|
+
".bytes": AssetType.TEXT_ASSET,
|
|
90
|
+
".csv": AssetType.TEXT_ASSET,
|
|
91
|
+
# Models
|
|
92
|
+
".fbx": AssetType.MODEL,
|
|
93
|
+
".obj": AssetType.MODEL,
|
|
94
|
+
".blend": AssetType.MODEL,
|
|
95
|
+
# Video
|
|
96
|
+
".mp4": AssetType.VIDEO_CLIP,
|
|
97
|
+
".webm": AssetType.VIDEO_CLIP,
|
|
98
|
+
".mov": AssetType.VIDEO_CLIP,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Field name patterns to expected asset types
|
|
103
|
+
# Patterns are checked in order, first match wins
|
|
104
|
+
FIELD_NAME_TO_EXPECTED_TYPES: list[tuple[re.Pattern, list[AssetType]]] = [
|
|
105
|
+
# Sprite fields (including camelCase like playerSprite)
|
|
106
|
+
(re.compile(r"(?i)(^|_)sprite($|s$|_)|[a-z]Sprite(s)?$"), [AssetType.SPRITE]),
|
|
107
|
+
(re.compile(r"^m_Sprite$"), [AssetType.SPRITE]),
|
|
108
|
+
# Audio fields
|
|
109
|
+
(re.compile(r"(?i)(audio|sound|clip|music|sfx)"), [AssetType.AUDIO_CLIP]),
|
|
110
|
+
(re.compile(r"^m_audioClip$", re.IGNORECASE), [AssetType.AUDIO_CLIP]),
|
|
111
|
+
# Material fields
|
|
112
|
+
(re.compile(r"(?i)(^|_)material($|s$|_)"), [AssetType.MATERIAL]),
|
|
113
|
+
(re.compile(r"^m_Material"), [AssetType.MATERIAL]),
|
|
114
|
+
(re.compile(r"^m_Materials"), [AssetType.MATERIAL]),
|
|
115
|
+
# Prefab fields
|
|
116
|
+
(re.compile(r"(?i)prefab"), [AssetType.PREFAB]),
|
|
117
|
+
# Script fields
|
|
118
|
+
(re.compile(r"^m_Script$"), [AssetType.SCRIPT]),
|
|
119
|
+
# Animator fields
|
|
120
|
+
(re.compile(r"(?i)(animator|controller)"), [AssetType.ANIMATOR_CONTROLLER]),
|
|
121
|
+
(re.compile(r"^m_Controller$"), [AssetType.ANIMATOR_CONTROLLER]),
|
|
122
|
+
# Animation fields
|
|
123
|
+
(re.compile(r"(?i)(anim|animation)(?!.*controller)"), [AssetType.ANIMATION_CLIP]),
|
|
124
|
+
# Font fields
|
|
125
|
+
(re.compile(r"(?i)font"), [AssetType.FONT]),
|
|
126
|
+
# Texture fields (general textures, not sprites)
|
|
127
|
+
(re.compile(r"(?i)texture"), [AssetType.TEXTURE, AssetType.SPRITE]),
|
|
128
|
+
(re.compile(r"^m_Texture"), [AssetType.TEXTURE, AssetType.SPRITE]),
|
|
129
|
+
# Mesh/Model fields
|
|
130
|
+
(re.compile(r"(?i)(mesh|model)"), [AssetType.MODEL]),
|
|
131
|
+
# Video fields
|
|
132
|
+
(re.compile(r"(?i)video"), [AssetType.VIDEO_CLIP]),
|
|
133
|
+
# ScriptableObject fields (generic data references)
|
|
134
|
+
(re.compile(r"(?i)(data|config|settings|so$)"), [AssetType.SCRIPTABLE_OBJECT]),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class AssetTypeMismatchError(ValueError):
|
|
139
|
+
"""Error raised when asset type doesn't match expected field type."""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
field_name: str,
|
|
144
|
+
asset_path: str,
|
|
145
|
+
expected_types: list[AssetType],
|
|
146
|
+
actual_type: AssetType,
|
|
147
|
+
):
|
|
148
|
+
self.field_name = field_name
|
|
149
|
+
self.asset_path = asset_path
|
|
150
|
+
self.expected_types = expected_types
|
|
151
|
+
self.actual_type = actual_type
|
|
152
|
+
expected_str = ", ".join(t.value for t in expected_types)
|
|
153
|
+
super().__init__(
|
|
154
|
+
f"Type mismatch for field '{field_name}': "
|
|
155
|
+
f"expected {expected_str}, but '{asset_path}' is {actual_type.value}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_asset_type_from_extension(extension: str) -> AssetType:
|
|
160
|
+
"""Get the asset type from a file extension.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
extension: File extension (with or without leading dot)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
AssetType for the extension
|
|
167
|
+
"""
|
|
168
|
+
ext = extension.lower()
|
|
169
|
+
if not ext.startswith("."):
|
|
170
|
+
ext = "." + ext
|
|
171
|
+
return EXTENSION_TO_ASSET_TYPE.get(ext, AssetType.UNKNOWN)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_expected_types_for_field(field_name: str) -> list[AssetType] | None:
|
|
175
|
+
"""Get expected asset types for a field name.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
field_name: The field name (e.g., 'm_Sprite', 'audioClip', 'enemyPrefab')
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of acceptable AssetTypes, or None if no expectation
|
|
182
|
+
"""
|
|
183
|
+
for pattern, types in FIELD_NAME_TO_EXPECTED_TYPES:
|
|
184
|
+
if pattern.search(field_name):
|
|
185
|
+
return types
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def validate_asset_type_for_field(
|
|
190
|
+
field_name: str,
|
|
191
|
+
asset_path: str,
|
|
192
|
+
asset_type: AssetType,
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Validate that an asset type is compatible with a field.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
field_name: The field name being set
|
|
198
|
+
asset_path: The asset path (for error messages)
|
|
199
|
+
asset_type: The actual asset type
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
AssetTypeMismatchError: If the type doesn't match expectations
|
|
203
|
+
"""
|
|
204
|
+
expected_types = get_expected_types_for_field(field_name)
|
|
205
|
+
|
|
206
|
+
if expected_types is None:
|
|
207
|
+
# No expectation for this field, allow anything
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if asset_type in expected_types:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# Special case: SPRITE can be used where TEXTURE is expected
|
|
214
|
+
if asset_type == AssetType.SPRITE and AssetType.TEXTURE in expected_types:
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
# Special case: TEXTURE can be used where SPRITE is expected
|
|
218
|
+
if asset_type == AssetType.TEXTURE and AssetType.SPRITE in expected_types:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
raise AssetTypeMismatchError(
|
|
222
|
+
field_name=field_name,
|
|
223
|
+
asset_path=asset_path,
|
|
224
|
+
expected_types=expected_types,
|
|
225
|
+
actual_type=asset_type,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Standard fileIDs for different asset types
|
|
230
|
+
ASSET_TYPE_FILE_IDS: dict[str, int] = {
|
|
231
|
+
# Scripts
|
|
232
|
+
".cs": 11500000,
|
|
233
|
+
# Textures/Sprites
|
|
234
|
+
".png": 21300000, # Sprite (Single mode default)
|
|
235
|
+
".jpg": 21300000,
|
|
236
|
+
".jpeg": 21300000,
|
|
237
|
+
".tga": 21300000,
|
|
238
|
+
".psd": 21300000,
|
|
239
|
+
".tiff": 21300000,
|
|
240
|
+
".gif": 21300000,
|
|
241
|
+
".bmp": 21300000,
|
|
242
|
+
".exr": 21300000,
|
|
243
|
+
".hdr": 21300000,
|
|
244
|
+
# Audio
|
|
245
|
+
".wav": 8300000,
|
|
246
|
+
".mp3": 8300000,
|
|
247
|
+
".ogg": 8300000,
|
|
248
|
+
".aiff": 8300000,
|
|
249
|
+
".aif": 8300000,
|
|
250
|
+
".flac": 8300000,
|
|
251
|
+
# Materials
|
|
252
|
+
".mat": 2100000,
|
|
253
|
+
# Animations
|
|
254
|
+
".anim": 7400000,
|
|
255
|
+
".controller": 9100000,
|
|
256
|
+
# Fonts
|
|
257
|
+
".ttf": 12800000,
|
|
258
|
+
".otf": 12800000,
|
|
259
|
+
".fon": 12800000,
|
|
260
|
+
# Shaders
|
|
261
|
+
".shader": 4800000,
|
|
262
|
+
".shadergraph": 11400000,
|
|
263
|
+
# ScriptableObjects
|
|
264
|
+
".asset": 11400000,
|
|
265
|
+
# Text/Data
|
|
266
|
+
".txt": 4900000,
|
|
267
|
+
".json": 4900000,
|
|
268
|
+
".xml": 4900000,
|
|
269
|
+
".bytes": 4900000,
|
|
270
|
+
".csv": 4900000,
|
|
271
|
+
# Models (main mesh)
|
|
272
|
+
".fbx": 10000, # Mesh fileID varies, but ~100000 for main mesh
|
|
273
|
+
".obj": 10000,
|
|
274
|
+
".blend": 10000,
|
|
275
|
+
# Video
|
|
276
|
+
".mp4": 32900000,
|
|
277
|
+
".webm": 32900000,
|
|
278
|
+
".mov": 32900000,
|
|
279
|
+
# Prefabs - need special handling
|
|
280
|
+
".prefab": None, # Requires parsing the prefab
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# Reference type values
|
|
284
|
+
REF_TYPE_ASSET = 3 # External asset
|
|
285
|
+
REF_TYPE_BUILTIN = 2 # Built-in or internal
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@dataclass
|
|
289
|
+
class AssetResolveResult:
|
|
290
|
+
"""Result of resolving an asset path to a Unity reference."""
|
|
291
|
+
|
|
292
|
+
file_id: int
|
|
293
|
+
guid: str
|
|
294
|
+
ref_type: int = REF_TYPE_ASSET
|
|
295
|
+
asset_path: str | None = None
|
|
296
|
+
sub_asset: str | None = None
|
|
297
|
+
|
|
298
|
+
def to_dict(self) -> dict[str, Any]:
|
|
299
|
+
"""Convert to Unity reference dictionary format."""
|
|
300
|
+
return {
|
|
301
|
+
"fileID": self.file_id,
|
|
302
|
+
"guid": self.guid,
|
|
303
|
+
"type": self.ref_type,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def is_asset_reference(value: str) -> bool:
|
|
308
|
+
"""Check if a value is an asset reference (starts with @)."""
|
|
309
|
+
return isinstance(value, str) and value.startswith("@")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def parse_asset_reference(value: str) -> tuple[str, str | None]:
|
|
313
|
+
"""Parse an asset reference into path and optional sub-asset.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
value: Asset reference string (e.g., "@Assets/Sprites/atlas.png:idle_0")
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Tuple of (asset_path, sub_asset_name or None)
|
|
320
|
+
|
|
321
|
+
Examples:
|
|
322
|
+
"@Assets/Scripts/Player.cs" -> ("Assets/Scripts/Player.cs", None)
|
|
323
|
+
"@Assets/Sprites/atlas.png:idle_0" -> ("Assets/Sprites/atlas.png", "idle_0")
|
|
324
|
+
"""
|
|
325
|
+
if not value.startswith("@"):
|
|
326
|
+
return value, None
|
|
327
|
+
|
|
328
|
+
path = value[1:] # Remove @ prefix
|
|
329
|
+
|
|
330
|
+
# Check for sub-asset separator (:)
|
|
331
|
+
if ":" in path:
|
|
332
|
+
# Find the last colon that's after the file extension
|
|
333
|
+
# This handles Windows paths like C:\path\file.png
|
|
334
|
+
parts = path.rsplit(":", 1)
|
|
335
|
+
# Verify the first part looks like a file path (has extension)
|
|
336
|
+
if "." in parts[0]:
|
|
337
|
+
return parts[0], parts[1]
|
|
338
|
+
|
|
339
|
+
return path, None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def get_guid_from_meta(meta_path: Path) -> str | None:
|
|
343
|
+
"""Extract GUID from a .meta file.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
meta_path: Path to the .meta file
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
GUID string or None if not found
|
|
350
|
+
"""
|
|
351
|
+
try:
|
|
352
|
+
content = meta_path.read_text(encoding="utf-8")
|
|
353
|
+
match = META_GUID_PATTERN.search(content)
|
|
354
|
+
if match:
|
|
355
|
+
return match.group(1)
|
|
356
|
+
except OSError:
|
|
357
|
+
pass
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def get_sprite_file_id(meta_path: Path, sub_sprite_name: str | None = None) -> int | None:
|
|
362
|
+
"""Get the fileID for a sprite reference.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
meta_path: Path to the sprite's .meta file
|
|
366
|
+
sub_sprite_name: For Multiple mode, the name of the sub-sprite
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
fileID or None if not found
|
|
370
|
+
"""
|
|
371
|
+
try:
|
|
372
|
+
content = meta_path.read_text(encoding="utf-8")
|
|
373
|
+
except OSError:
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
# Check sprite mode
|
|
377
|
+
sprite_mode_match = re.search(r"^\s*spriteMode:\s*(\d+)", content, re.MULTILINE)
|
|
378
|
+
sprite_mode = int(sprite_mode_match.group(1)) if sprite_mode_match else 1
|
|
379
|
+
|
|
380
|
+
if sprite_mode == 1: # Single mode
|
|
381
|
+
return 21300000
|
|
382
|
+
|
|
383
|
+
if sprite_mode == 2: # Multiple mode
|
|
384
|
+
if sub_sprite_name:
|
|
385
|
+
# Look up in internalIDToNameTable
|
|
386
|
+
pattern = re.compile(
|
|
387
|
+
r"-\s+first:\s*\n\s+213:\s*(-?\d+)\s*\n\s+second:\s*" + re.escape(sub_sprite_name),
|
|
388
|
+
re.MULTILINE,
|
|
389
|
+
)
|
|
390
|
+
match = pattern.search(content)
|
|
391
|
+
if match:
|
|
392
|
+
return int(match.group(1))
|
|
393
|
+
|
|
394
|
+
# Also try spriteSheet.sprites section
|
|
395
|
+
sprite_pattern = re.compile(
|
|
396
|
+
r"name:\s*" + re.escape(sub_sprite_name) + r".*?internalID:\s*(-?\d+)",
|
|
397
|
+
re.DOTALL,
|
|
398
|
+
)
|
|
399
|
+
match = sprite_pattern.search(content)
|
|
400
|
+
if match:
|
|
401
|
+
return int(match.group(1))
|
|
402
|
+
|
|
403
|
+
return None
|
|
404
|
+
else:
|
|
405
|
+
# Return first sprite's ID
|
|
406
|
+
pattern = re.compile(
|
|
407
|
+
r"-\s+first:\s*\n\s+213:\s*(-?\d+)",
|
|
408
|
+
re.MULTILINE,
|
|
409
|
+
)
|
|
410
|
+
match = pattern.search(content)
|
|
411
|
+
if match:
|
|
412
|
+
return int(match.group(1))
|
|
413
|
+
|
|
414
|
+
return 21300000 # Fallback
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def get_prefab_root_file_id(prefab_path: Path) -> int | None:
|
|
418
|
+
"""Get the root GameObject's fileID from a prefab.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
prefab_path: Path to the .prefab file
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
fileID of the root GameObject, or None if not found
|
|
425
|
+
"""
|
|
426
|
+
try:
|
|
427
|
+
content = prefab_path.read_text(encoding="utf-8")
|
|
428
|
+
except OSError:
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
# Find all GameObject declarations and their transforms
|
|
432
|
+
# Pattern: --- !u!1 &<fileID>
|
|
433
|
+
game_objects: list[int] = []
|
|
434
|
+
pattern = re.compile(r"^--- !u!1 &(\d+)", re.MULTILINE)
|
|
435
|
+
for match in pattern.finditer(content):
|
|
436
|
+
game_objects.append(int(match.group(1)))
|
|
437
|
+
|
|
438
|
+
if not game_objects:
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
# The root is typically the first GameObject, but let's verify
|
|
442
|
+
# by checking which GameObject has no parent Transform
|
|
443
|
+
# For simplicity, return the first one (most prefabs have root first)
|
|
444
|
+
return game_objects[0]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def resolve_asset_reference(
|
|
448
|
+
value: str,
|
|
449
|
+
project_root: Path | None = None,
|
|
450
|
+
auto_generate_meta: bool = True,
|
|
451
|
+
) -> AssetResolveResult | None:
|
|
452
|
+
"""Resolve an asset reference to a Unity reference.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
value: Asset reference string (e.g., "@Assets/Scripts/Player.cs")
|
|
456
|
+
project_root: Unity project root for resolving relative paths
|
|
457
|
+
auto_generate_meta: If True, automatically generate .meta file if missing
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
AssetResolveResult with fileID and guid, or None if resolution failed
|
|
461
|
+
|
|
462
|
+
Examples:
|
|
463
|
+
>>> result = resolve_asset_reference("@Assets/Scripts/Player.cs", project_root)
|
|
464
|
+
>>> print(result.to_dict())
|
|
465
|
+
{'fileID': 11500000, 'guid': 'abc123...', 'type': 3}
|
|
466
|
+
"""
|
|
467
|
+
import logging
|
|
468
|
+
|
|
469
|
+
logger = logging.getLogger(__name__)
|
|
470
|
+
|
|
471
|
+
if not is_asset_reference(value):
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
asset_path, sub_asset = parse_asset_reference(value)
|
|
475
|
+
|
|
476
|
+
# Resolve to absolute path
|
|
477
|
+
if project_root:
|
|
478
|
+
full_path = project_root / asset_path
|
|
479
|
+
else:
|
|
480
|
+
full_path = Path(asset_path)
|
|
481
|
+
|
|
482
|
+
meta_path = Path(str(full_path) + ".meta")
|
|
483
|
+
|
|
484
|
+
# Check if meta file exists, auto-generate if needed
|
|
485
|
+
if not meta_path.is_file():
|
|
486
|
+
if auto_generate_meta and full_path.is_file():
|
|
487
|
+
# Auto-generate .meta file
|
|
488
|
+
from unityflow.meta_generator import generate_meta_file
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
generate_meta_file(full_path)
|
|
492
|
+
logger.info(f"Auto-generated .meta file for: {asset_path}")
|
|
493
|
+
except Exception as e:
|
|
494
|
+
logger.warning(f"Failed to auto-generate .meta for {asset_path}: {e}")
|
|
495
|
+
return None
|
|
496
|
+
else:
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
# Get GUID from meta file
|
|
500
|
+
guid = get_guid_from_meta(meta_path)
|
|
501
|
+
if not guid:
|
|
502
|
+
return None
|
|
503
|
+
|
|
504
|
+
# Determine fileID based on asset type
|
|
505
|
+
suffix = full_path.suffix.lower()
|
|
506
|
+
file_id: int | None = None
|
|
507
|
+
|
|
508
|
+
# Special handling for sprites (check mode and sub-sprite)
|
|
509
|
+
if suffix in (".png", ".jpg", ".jpeg", ".tga", ".psd", ".tiff", ".gif", ".bmp", ".exr", ".hdr"):
|
|
510
|
+
file_id = get_sprite_file_id(meta_path, sub_asset)
|
|
511
|
+
|
|
512
|
+
# Special handling for prefabs
|
|
513
|
+
elif suffix == ".prefab":
|
|
514
|
+
file_id = get_prefab_root_file_id(full_path)
|
|
515
|
+
|
|
516
|
+
# Use standard fileID for known types
|
|
517
|
+
elif suffix in ASSET_TYPE_FILE_IDS:
|
|
518
|
+
file_id = ASSET_TYPE_FILE_IDS[suffix]
|
|
519
|
+
|
|
520
|
+
if file_id is None:
|
|
521
|
+
return None
|
|
522
|
+
|
|
523
|
+
return AssetResolveResult(
|
|
524
|
+
file_id=file_id,
|
|
525
|
+
guid=guid,
|
|
526
|
+
ref_type=REF_TYPE_ASSET,
|
|
527
|
+
asset_path=asset_path,
|
|
528
|
+
sub_asset=sub_asset,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def resolve_value(
|
|
533
|
+
value: Any,
|
|
534
|
+
project_root: Path | None = None,
|
|
535
|
+
field_name: str | None = None,
|
|
536
|
+
) -> Any:
|
|
537
|
+
"""Resolve a value, converting asset references to Unity reference dicts.
|
|
538
|
+
|
|
539
|
+
Recursively processes dicts and lists, converting any string values
|
|
540
|
+
that start with @ to asset references.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
value: Value to process
|
|
544
|
+
project_root: Unity project root for resolving relative paths
|
|
545
|
+
field_name: The field name being set (for type validation)
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Processed value with asset references resolved
|
|
549
|
+
|
|
550
|
+
Raises:
|
|
551
|
+
ValueError: If an asset reference cannot be resolved
|
|
552
|
+
AssetTypeMismatchError: If the asset type doesn't match the field type
|
|
553
|
+
"""
|
|
554
|
+
if isinstance(value, str):
|
|
555
|
+
if is_asset_reference(value):
|
|
556
|
+
result = resolve_asset_reference(value, project_root)
|
|
557
|
+
if result is None:
|
|
558
|
+
raise ValueError(f"Could not resolve asset reference: {value}")
|
|
559
|
+
|
|
560
|
+
# Validate asset type if field name is provided
|
|
561
|
+
if field_name and result.asset_path:
|
|
562
|
+
suffix = Path(result.asset_path).suffix.lower()
|
|
563
|
+
asset_type = get_asset_type_from_extension(suffix)
|
|
564
|
+
validate_asset_type_for_field(field_name, result.asset_path, asset_type)
|
|
565
|
+
|
|
566
|
+
return result.to_dict()
|
|
567
|
+
return value
|
|
568
|
+
|
|
569
|
+
elif isinstance(value, dict):
|
|
570
|
+
# For dicts, use the key as the field name for validation
|
|
571
|
+
return {k: resolve_value(v, project_root, field_name=k) for k, v in value.items()}
|
|
572
|
+
|
|
573
|
+
elif isinstance(value, list):
|
|
574
|
+
# For lists, use the parent field name
|
|
575
|
+
return [resolve_value(item, project_root, field_name) for item in value]
|
|
576
|
+
|
|
577
|
+
return value
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def get_asset_info(
|
|
581
|
+
asset_path: str,
|
|
582
|
+
project_root: Path | None = None,
|
|
583
|
+
) -> dict[str, Any] | None:
|
|
584
|
+
"""Get detailed information about an asset.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
asset_path: Path to the asset (without @ prefix)
|
|
588
|
+
project_root: Unity project root
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Dictionary with asset info, or None if not found
|
|
592
|
+
"""
|
|
593
|
+
if project_root:
|
|
594
|
+
full_path = project_root / asset_path
|
|
595
|
+
else:
|
|
596
|
+
full_path = Path(asset_path)
|
|
597
|
+
|
|
598
|
+
meta_path = Path(str(full_path) + ".meta")
|
|
599
|
+
|
|
600
|
+
if not meta_path.is_file():
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
guid = get_guid_from_meta(meta_path)
|
|
604
|
+
if not guid:
|
|
605
|
+
return None
|
|
606
|
+
|
|
607
|
+
suffix = full_path.suffix.lower()
|
|
608
|
+
info: dict[str, Any] = {
|
|
609
|
+
"path": asset_path,
|
|
610
|
+
"guid": guid,
|
|
611
|
+
"type": suffix[1:] if suffix else "unknown",
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
# Add sprite-specific info
|
|
615
|
+
if suffix in (".png", ".jpg", ".jpeg", ".tga", ".psd"):
|
|
616
|
+
try:
|
|
617
|
+
content = meta_path.read_text(encoding="utf-8")
|
|
618
|
+
mode_match = re.search(r"^\s*spriteMode:\s*(\d+)", content, re.MULTILINE)
|
|
619
|
+
if mode_match:
|
|
620
|
+
mode = int(mode_match.group(1))
|
|
621
|
+
info["spriteMode"] = "Single" if mode == 1 else "Multiple" if mode == 2 else "None"
|
|
622
|
+
|
|
623
|
+
if mode == 2:
|
|
624
|
+
# Extract sub-sprite names
|
|
625
|
+
sub_sprites: list[str] = []
|
|
626
|
+
pattern = re.compile(
|
|
627
|
+
r"-\s+first:\s*\n\s+213:\s*(-?\d+)\s*\n\s+second:\s*(\S+)",
|
|
628
|
+
re.MULTILINE,
|
|
629
|
+
)
|
|
630
|
+
for match in pattern.finditer(content):
|
|
631
|
+
sub_sprites.append(match.group(2))
|
|
632
|
+
info["subSprites"] = sub_sprites
|
|
633
|
+
except OSError:
|
|
634
|
+
pass
|
|
635
|
+
|
|
636
|
+
return info
|