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.
@@ -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