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,1291 @@
1
+ """Unity .meta file generator.
2
+
3
+ Generate .meta files for Unity assets without opening Unity Editor.
4
+ Supports various asset types with appropriate importer settings.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import uuid
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ class AssetType(Enum):
18
+ """Unity asset types with their corresponding importers."""
19
+
20
+ FOLDER = "folder"
21
+ SCRIPT = "script" # .cs, .js
22
+ TEXTURE = "texture" # .png, .jpg, .jpeg, .tga, .psd, .tiff, .bmp, .gif, .exr, .hdr
23
+ AUDIO = "audio" # .wav, .mp3, .ogg, .aiff, .flac
24
+ VIDEO = "video" # .mp4, .mov, .avi, .webm
25
+ MODEL = "model" # .fbx, .obj, .dae, .3ds, .blend
26
+ SHADER = "shader" # .shader, .cginc, .hlsl, .compute
27
+ MATERIAL = "material" # .mat (Unity YAML)
28
+ PREFAB = "prefab" # .prefab (Unity YAML)
29
+ SCENE = "scene" # .unity (Unity YAML)
30
+ SCRIPTABLE_OBJECT = "scriptable_object" # .asset (Unity YAML)
31
+ ANIMATION = "animation" # .anim, .controller, .overrideController
32
+ FONT = "font" # .ttf, .otf
33
+ TEXT = "text" # .txt, .json, .xml, .csv, .bytes
34
+ PLUGIN = "plugin" # .dll, .so, .dylib
35
+ DEFAULT = "default" # fallback
36
+
37
+
38
+ # File extension to asset type mapping
39
+ EXTENSION_TO_TYPE: dict[str, AssetType] = {
40
+ # Scripts
41
+ ".cs": AssetType.SCRIPT,
42
+ ".js": AssetType.SCRIPT,
43
+ # Textures
44
+ ".png": AssetType.TEXTURE,
45
+ ".jpg": AssetType.TEXTURE,
46
+ ".jpeg": AssetType.TEXTURE,
47
+ ".tga": AssetType.TEXTURE,
48
+ ".psd": AssetType.TEXTURE,
49
+ ".tiff": AssetType.TEXTURE,
50
+ ".tif": AssetType.TEXTURE,
51
+ ".bmp": AssetType.TEXTURE,
52
+ ".gif": AssetType.TEXTURE,
53
+ ".exr": AssetType.TEXTURE,
54
+ ".hdr": AssetType.TEXTURE,
55
+ # Audio
56
+ ".wav": AssetType.AUDIO,
57
+ ".mp3": AssetType.AUDIO,
58
+ ".ogg": AssetType.AUDIO,
59
+ ".aiff": AssetType.AUDIO,
60
+ ".aif": AssetType.AUDIO,
61
+ ".flac": AssetType.AUDIO,
62
+ ".m4a": AssetType.AUDIO,
63
+ # Video
64
+ ".mp4": AssetType.VIDEO,
65
+ ".mov": AssetType.VIDEO,
66
+ ".avi": AssetType.VIDEO,
67
+ ".webm": AssetType.VIDEO,
68
+ # 3D Models
69
+ ".fbx": AssetType.MODEL,
70
+ ".obj": AssetType.MODEL,
71
+ ".dae": AssetType.MODEL,
72
+ ".3ds": AssetType.MODEL,
73
+ ".blend": AssetType.MODEL,
74
+ ".max": AssetType.MODEL,
75
+ ".ma": AssetType.MODEL,
76
+ ".mb": AssetType.MODEL,
77
+ # Shaders
78
+ ".shader": AssetType.SHADER,
79
+ ".cginc": AssetType.SHADER,
80
+ ".hlsl": AssetType.SHADER,
81
+ ".glsl": AssetType.SHADER,
82
+ ".compute": AssetType.SHADER,
83
+ # Unity YAML assets
84
+ ".mat": AssetType.MATERIAL,
85
+ ".prefab": AssetType.PREFAB,
86
+ ".unity": AssetType.SCENE,
87
+ ".asset": AssetType.SCRIPTABLE_OBJECT,
88
+ # Animation
89
+ ".anim": AssetType.ANIMATION,
90
+ ".controller": AssetType.ANIMATION,
91
+ ".overrideController": AssetType.ANIMATION,
92
+ ".playable": AssetType.ANIMATION,
93
+ ".mask": AssetType.ANIMATION,
94
+ # Fonts
95
+ ".ttf": AssetType.FONT,
96
+ ".otf": AssetType.FONT,
97
+ ".fon": AssetType.FONT,
98
+ # Text/Data
99
+ ".txt": AssetType.TEXT,
100
+ ".json": AssetType.TEXT,
101
+ ".xml": AssetType.TEXT,
102
+ ".csv": AssetType.TEXT,
103
+ ".bytes": AssetType.TEXT,
104
+ ".html": AssetType.TEXT,
105
+ ".htm": AssetType.TEXT,
106
+ ".yaml": AssetType.TEXT,
107
+ ".yml": AssetType.TEXT,
108
+ # Plugins
109
+ ".dll": AssetType.PLUGIN,
110
+ ".so": AssetType.PLUGIN,
111
+ ".dylib": AssetType.PLUGIN,
112
+ }
113
+
114
+
115
+ def generate_guid(seed: str | None = None) -> str:
116
+ """Generate a Unity-compatible GUID (32 hex characters).
117
+
118
+ Args:
119
+ seed: Optional seed string for deterministic GUID generation.
120
+ If provided, the same seed always produces the same GUID.
121
+ If None, generates a random GUID.
122
+
123
+ Returns:
124
+ 32-character lowercase hex string (Unity GUID format)
125
+ """
126
+ if seed is not None:
127
+ # Deterministic GUID based on seed (useful for reproducible builds)
128
+ hash_bytes = hashlib.md5(seed.encode("utf-8")).digest()
129
+ return hash_bytes.hex()
130
+ else:
131
+ # Random GUID
132
+ return uuid.uuid4().hex
133
+
134
+
135
+ def detect_asset_type(path: Path) -> AssetType:
136
+ """Detect the asset type from file path.
137
+
138
+ Args:
139
+ path: Path to the asset file
140
+
141
+ Returns:
142
+ Detected AssetType
143
+ """
144
+ if path.is_dir():
145
+ return AssetType.FOLDER
146
+
147
+ ext = path.suffix.lower()
148
+ return EXTENSION_TO_TYPE.get(ext, AssetType.DEFAULT)
149
+
150
+
151
+ @dataclass
152
+ class MetaFileOptions:
153
+ """Options for meta file generation."""
154
+
155
+ # Common options
156
+ guid: str | None = None # Auto-generated if None
157
+ labels: list[str] = field(default_factory=list)
158
+ asset_bundle_name: str = ""
159
+ asset_bundle_variant: str = ""
160
+
161
+ # Texture options
162
+ texture_type: str = "Default" # Default, NormalMap, Sprite, Cursor, Cookie, Lightmap, etc.
163
+ sprite_mode: int = 1 # 0=None, 1=Single, 2=Multiple
164
+ sprite_pixels_per_unit: int = 100
165
+ sprite_pivot: tuple[float, float] = (0.5, 0.5)
166
+ filter_mode: int = 1 # 0=Point, 1=Bilinear, 2=Trilinear
167
+ max_texture_size: int = 2048
168
+ texture_compression: str = "Compressed"
169
+
170
+ # Audio options
171
+ load_type: int = 0 # 0=DecompressOnLoad, 1=CompressedInMemory, 2=Streaming
172
+ force_mono: bool = False
173
+
174
+ # Model options
175
+ import_materials: bool = True
176
+ import_animation: bool = True
177
+ mesh_compression: int = 0 # 0=Off, 1=Low, 2=Medium, 3=High
178
+
179
+ # Script options
180
+ execution_order: int = 0
181
+ icon: str = ""
182
+
183
+ # Use seed for deterministic GUID generation
184
+ guid_seed: str | None = None
185
+
186
+
187
+ def _generate_folder_meta(guid: str, options: MetaFileOptions) -> str:
188
+ """Generate meta file content for a folder."""
189
+ return f"""fileFormatVersion: 2
190
+ guid: {guid}
191
+ folderAsset: yes
192
+ DefaultImporter:
193
+ externalObjects: {{}}
194
+ userData:
195
+ assetBundleName: {options.asset_bundle_name}
196
+ assetBundleVariant: {options.asset_bundle_variant}
197
+ """
198
+
199
+
200
+ def _generate_script_meta(guid: str, options: MetaFileOptions) -> str:
201
+ """Generate meta file content for a C# script."""
202
+ icon_line = f" icon: {options.icon}" if options.icon else " icon: {instanceID: 0}"
203
+ return f"""fileFormatVersion: 2
204
+ guid: {guid}
205
+ MonoImporter:
206
+ externalObjects: {{}}
207
+ serializedVersion: 2
208
+ defaultReferences: []
209
+ executionOrder: {options.execution_order}
210
+ {icon_line}
211
+ userData:
212
+ assetBundleName: {options.asset_bundle_name}
213
+ assetBundleVariant: {options.asset_bundle_variant}
214
+ """
215
+
216
+
217
+ def _generate_texture_meta(guid: str, options: MetaFileOptions) -> str:
218
+ """Generate meta file content for a texture."""
219
+ # Determine texture import settings based on texture type
220
+ sprite_mode = options.sprite_mode if options.texture_type == "Sprite" else 0
221
+
222
+ return f"""fileFormatVersion: 2
223
+ guid: {guid}
224
+ TextureImporter:
225
+ internalIDToNameTable: []
226
+ externalObjects: {{}}
227
+ serializedVersion: 12
228
+ mipmaps:
229
+ mipMapMode: 0
230
+ enableMipMap: 1
231
+ sRGBTexture: 1
232
+ linearTexture: 0
233
+ fadeOut: 0
234
+ borderMipMap: 0
235
+ mipMapsPreserveCoverage: 0
236
+ alphaTestReferenceValue: 0.5
237
+ mipMapFadeDistanceStart: 1
238
+ mipMapFadeDistanceEnd: 3
239
+ bumpmap:
240
+ convertToNormalMap: 0
241
+ externalNormalMap: 0
242
+ heightScale: 0.25
243
+ normalMapFilter: 0
244
+ flipGreenChannel: 0
245
+ isReadable: 0
246
+ streamingMipmaps: 0
247
+ streamingMipmapsPriority: 0
248
+ vTOnly: 0
249
+ ignoreMipmapLimit: 0
250
+ grayScaleToAlpha: 0
251
+ generateCubemap: 6
252
+ cubemapConvolution: 0
253
+ seamlessCubemap: 0
254
+ textureFormat: 1
255
+ maxTextureSize: {options.max_texture_size}
256
+ textureSettings:
257
+ serializedVersion: 2
258
+ filterMode: {options.filter_mode}
259
+ aniso: 1
260
+ mipBias: 0
261
+ wrapU: 0
262
+ wrapV: 0
263
+ wrapW: 0
264
+ nPOTScale: 1
265
+ lightmap: 0
266
+ compressionQuality: 50
267
+ spriteMode: {sprite_mode}
268
+ spriteExtrude: 1
269
+ spriteMeshType: 1
270
+ alignment: 0
271
+ spritePivot: {{x: {options.sprite_pivot[0]}, y: {options.sprite_pivot[1]}}}
272
+ spritePixelsToUnits: {options.sprite_pixels_per_unit}
273
+ spriteBorder: {{x: 0, y: 0, z: 0, w: 0}}
274
+ spriteGenerateFallbackPhysicsShape: 1
275
+ alphaUsage: 1
276
+ alphaIsTransparency: 0
277
+ spriteTessellationDetail: -1
278
+ textureType: {_get_texture_type_id(options.texture_type)}
279
+ textureShape: 1
280
+ singleChannelComponent: 0
281
+ flipbookRows: 1
282
+ flipbookColumns: 1
283
+ maxTextureSizeSet: 0
284
+ compressionQualitySet: 0
285
+ textureFormatSet: 0
286
+ ignorePngGamma: 0
287
+ applyGammaDecoding: 0
288
+ swizzle: 50462976
289
+ cookieLightType: 0
290
+ platformSettings:
291
+ - serializedVersion: 3
292
+ buildTarget: DefaultTexturePlatform
293
+ maxTextureSize: {options.max_texture_size}
294
+ resizeAlgorithm: 0
295
+ textureFormat: -1
296
+ textureCompression: 1
297
+ compressionQuality: 50
298
+ crunchedCompression: 0
299
+ allowsAlphaSplitting: 0
300
+ overridden: 0
301
+ ignorePlatformSupport: 0
302
+ androidETC2FallbackOverride: 0
303
+ forceMaximumCompressionQuality_BC6H_BC7: 0
304
+ spriteSheet:
305
+ serializedVersion: 2
306
+ sprites: []
307
+ outline: []
308
+ physicsShape: []
309
+ bones: []
310
+ spriteID:
311
+ internalID: 0
312
+ vertices: []
313
+ indices:
314
+ edges: []
315
+ weights: []
316
+ secondaryTextures: []
317
+ nameFileIdTable: {{}}
318
+ mipmapLimitGroupName:
319
+ pSDRemoveMatte: 0
320
+ userData:
321
+ assetBundleName: {options.asset_bundle_name}
322
+ assetBundleVariant: {options.asset_bundle_variant}
323
+ """
324
+
325
+
326
+ def _get_texture_type_id(texture_type: str) -> int:
327
+ """Convert texture type name to Unity's internal ID."""
328
+ type_map = {
329
+ "Default": 0,
330
+ "NormalMap": 1,
331
+ "GUI": 2, # Legacy
332
+ "Sprite": 8,
333
+ "Cursor": 7,
334
+ "Cookie": 4,
335
+ "Lightmap": 6,
336
+ "DirectionalLightmap": 11,
337
+ "Shadowmask": 12,
338
+ "SingleChannel": 10,
339
+ }
340
+ return type_map.get(texture_type, 0)
341
+
342
+
343
+ def _generate_audio_meta(guid: str, options: MetaFileOptions) -> str:
344
+ """Generate meta file content for an audio file."""
345
+ return f"""fileFormatVersion: 2
346
+ guid: {guid}
347
+ AudioImporter:
348
+ externalObjects: {{}}
349
+ serializedVersion: 7
350
+ defaultSettings:
351
+ serializedVersion: 2
352
+ loadType: {options.load_type}
353
+ sampleRateSetting: 0
354
+ sampleRateOverride: 44100
355
+ compressionFormat: 1
356
+ quality: 1
357
+ conversionMode: 0
358
+ preloadAudioData: 1
359
+ platformSettingOverrides: {{}}
360
+ forceToMono: {1 if options.force_mono else 0}
361
+ normalize: 1
362
+ loadInBackground: 0
363
+ ambisonic: 0
364
+ 3D: 1
365
+ userData:
366
+ assetBundleName: {options.asset_bundle_name}
367
+ assetBundleVariant: {options.asset_bundle_variant}
368
+ """
369
+
370
+
371
+ def _generate_video_meta(guid: str, options: MetaFileOptions) -> str:
372
+ """Generate meta file content for a video file."""
373
+ return f"""fileFormatVersion: 2
374
+ guid: {guid}
375
+ VideoClipImporter:
376
+ externalObjects: {{}}
377
+ serializedVersion: 2
378
+ frameRange: 0
379
+ startFrame: -1
380
+ endFrame: -1
381
+ colorSpace: 0
382
+ deinterlace: 0
383
+ encodeAlpha: 0
384
+ flipVertical: 0
385
+ flipHorizontal: 0
386
+ importAudio: 1
387
+ targetSettings: {{}}
388
+ userData:
389
+ assetBundleName: {options.asset_bundle_name}
390
+ assetBundleVariant: {options.asset_bundle_variant}
391
+ """
392
+
393
+
394
+ def _generate_model_meta(guid: str, options: MetaFileOptions) -> str:
395
+ """Generate meta file content for a 3D model."""
396
+ return f"""fileFormatVersion: 2
397
+ guid: {guid}
398
+ ModelImporter:
399
+ serializedVersion: 22200
400
+ internalIDToNameTable: []
401
+ externalObjects: {{}}
402
+ materials:
403
+ materialImportMode: {1 if options.import_materials else 0}
404
+ materialName: 0
405
+ materialSearch: 1
406
+ materialLocation: 1
407
+ animations:
408
+ legacyGenerateAnimations: 4
409
+ bakeSimulation: 0
410
+ resampleCurves: 1
411
+ optimizeGameObjects: 0
412
+ removeConstantScaleCurves: 0
413
+ motionNodeName:
414
+ rigImportErrors:
415
+ rigImportWarnings:
416
+ animationImportErrors:
417
+ animationImportWarnings:
418
+ animationRetargetingWarnings:
419
+ animationDoRetargetingWarnings: 0
420
+ importAnimatedCustomProperties: 0
421
+ importConstraints: 0
422
+ animationCompression: {options.mesh_compression}
423
+ animationRotationError: 0.5
424
+ animationPositionError: 0.5
425
+ animationScaleError: 0.5
426
+ animationWrapMode: 0
427
+ extraExposedTransformPaths: []
428
+ extraUserProperties: []
429
+ clipAnimations: []
430
+ isReadable: 0
431
+ meshes:
432
+ lODScreenPercentages: []
433
+ globalScale: 1
434
+ meshCompression: {options.mesh_compression}
435
+ addColliders: 0
436
+ useSRGBMaterialColor: 1
437
+ sortHierarchyByName: 1
438
+ importPhysicalCameras: 1
439
+ importVisibility: 1
440
+ importBlendShapes: 1
441
+ importCameras: 1
442
+ importLights: 1
443
+ nodeNameCollisionStrategy: 1
444
+ fileIdsGeneration: 2
445
+ swapUVChannels: 0
446
+ generateSecondaryUV: 0
447
+ useFileUnits: 1
448
+ keepQuads: 0
449
+ weldVertices: 1
450
+ bakeAxisConversion: 0
451
+ preserveHierarchy: 0
452
+ skinWeightsMode: 0
453
+ maxBonesPerVertex: 4
454
+ minBoneWeight: 0.001
455
+ optimizeMeshPolygons: 1
456
+ optimizeMeshVertices: 1
457
+ meshOptimizationFlags: -1
458
+ indexFormat: 0
459
+ secondaryUVAngleDistortion: 8
460
+ secondaryUVAreaDistortion: 15.000001
461
+ secondaryUVHardAngle: 88
462
+ secondaryUVMarginMethod: 1
463
+ secondaryUVMinLightmapResolution: 40
464
+ secondaryUVMinObjectScale: 1
465
+ secondaryUVPackMargin: 4
466
+ useFileScale: 1
467
+ strictVertexDataChecks: 0
468
+ tangentSpace:
469
+ normalSmoothAngle: 60
470
+ normalImportMode: 0
471
+ tangentImportMode: 3
472
+ normalCalculationMode: 4
473
+ legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
474
+ blendShapeNormalImportMode: 1
475
+ referencedClips: []
476
+ importAnimation: {1 if options.import_animation else 0}
477
+ humanDescription:
478
+ serializedVersion: 3
479
+ human: []
480
+ skeleton: []
481
+ armTwist: 0.5
482
+ foreArmTwist: 0.5
483
+ upperLegTwist: 0.5
484
+ legTwist: 0.5
485
+ armStretch: 0.05
486
+ legStretch: 0.05
487
+ feetSpacing: 0
488
+ globalScale: 1
489
+ rootMotionBoneName:
490
+ hasTranslationDoF: 0
491
+ hasExtraRoot: 0
492
+ skeletonHasParents: 1
493
+ lastHumanDescriptionAvatarSource: {{instanceID: 0}}
494
+ autoGenerateAvatarMappingIfUnspecified: 1
495
+ animationType: 2
496
+ humanoidOversampling: 1
497
+ avatarSetup: 0
498
+ addHumanoidExtraRootOnlyWhenUsingAvatar: 1
499
+ importBlendShapeDeformPercent: 1
500
+ remapMaterialsIfMaterialImportModeIsNone: 0
501
+ additionalBone: 0
502
+ userData:
503
+ assetBundleName: {options.asset_bundle_name}
504
+ assetBundleVariant: {options.asset_bundle_variant}
505
+ """
506
+
507
+
508
+ def _generate_shader_meta(guid: str, options: MetaFileOptions) -> str:
509
+ """Generate meta file content for a shader."""
510
+ return f"""fileFormatVersion: 2
511
+ guid: {guid}
512
+ ShaderImporter:
513
+ externalObjects: {{}}
514
+ defaultTextures: []
515
+ nonModifiableTextures: []
516
+ preprocessorOverride: 0
517
+ userData:
518
+ assetBundleName: {options.asset_bundle_name}
519
+ assetBundleVariant: {options.asset_bundle_variant}
520
+ """
521
+
522
+
523
+ def _generate_font_meta(guid: str, options: MetaFileOptions) -> str:
524
+ """Generate meta file content for a font file."""
525
+ return f"""fileFormatVersion: 2
526
+ guid: {guid}
527
+ TrueTypeFontImporter:
528
+ externalObjects: {{}}
529
+ serializedVersion: 4
530
+ fontSize: 16
531
+ forceTextureCase: -2
532
+ characterSpacing: 0
533
+ characterPadding: 1
534
+ includeFontData: 1
535
+ fontName:
536
+ fallbackFontReferences: []
537
+ fontNames: []
538
+ customCharacters:
539
+ fontRenderingMode: 0
540
+ ascentCalculationMode: 1
541
+ useLegacyBoundsCalculation: 0
542
+ shouldRoundAdvanceValue: 1
543
+ userData:
544
+ assetBundleName: {options.asset_bundle_name}
545
+ assetBundleVariant: {options.asset_bundle_variant}
546
+ """
547
+
548
+
549
+ def _generate_text_meta(guid: str, options: MetaFileOptions) -> str:
550
+ """Generate meta file content for a text file."""
551
+ return f"""fileFormatVersion: 2
552
+ guid: {guid}
553
+ TextScriptImporter:
554
+ externalObjects: {{}}
555
+ userData:
556
+ assetBundleName: {options.asset_bundle_name}
557
+ assetBundleVariant: {options.asset_bundle_variant}
558
+ """
559
+
560
+
561
+ def _generate_plugin_meta(guid: str, options: MetaFileOptions) -> str:
562
+ """Generate meta file content for a native plugin."""
563
+ return f"""fileFormatVersion: 2
564
+ guid: {guid}
565
+ PluginImporter:
566
+ externalObjects: {{}}
567
+ serializedVersion: 2
568
+ iconMap: {{}}
569
+ executionOrder: {{}}
570
+ defineConstraints: []
571
+ isPreloaded: 0
572
+ isOverridable: 0
573
+ isExplicitlyReferenced: 0
574
+ validateReferences: 1
575
+ platformData:
576
+ - first:
577
+ Any:
578
+ second:
579
+ enabled: 1
580
+ settings: {{}}
581
+ - first:
582
+ Editor: Editor
583
+ second:
584
+ enabled: 1
585
+ settings:
586
+ DefaultValueInitialized: true
587
+ userData:
588
+ assetBundleName: {options.asset_bundle_name}
589
+ assetBundleVariant: {options.asset_bundle_variant}
590
+ """
591
+
592
+
593
+ def _generate_default_meta(guid: str, options: MetaFileOptions) -> str:
594
+ """Generate meta file content for default/Unity YAML assets."""
595
+ return f"""fileFormatVersion: 2
596
+ guid: {guid}
597
+ DefaultImporter:
598
+ externalObjects: {{}}
599
+ userData:
600
+ assetBundleName: {options.asset_bundle_name}
601
+ assetBundleVariant: {options.asset_bundle_variant}
602
+ """
603
+
604
+
605
+ def _generate_native_format_meta(guid: str, options: MetaFileOptions) -> str:
606
+ """Generate meta file content for native Unity formats (prefab, scene, etc.)."""
607
+ return f"""fileFormatVersion: 2
608
+ guid: {guid}
609
+ NativeFormatImporter:
610
+ externalObjects: {{}}
611
+ mainObjectFileID: 100100000
612
+ userData:
613
+ assetBundleName: {options.asset_bundle_name}
614
+ assetBundleVariant: {options.asset_bundle_variant}
615
+ """
616
+
617
+
618
+ # Generator function mapping
619
+ META_GENERATORS: dict[AssetType, callable] = {
620
+ AssetType.FOLDER: _generate_folder_meta,
621
+ AssetType.SCRIPT: _generate_script_meta,
622
+ AssetType.TEXTURE: _generate_texture_meta,
623
+ AssetType.AUDIO: _generate_audio_meta,
624
+ AssetType.VIDEO: _generate_video_meta,
625
+ AssetType.MODEL: _generate_model_meta,
626
+ AssetType.SHADER: _generate_shader_meta,
627
+ AssetType.FONT: _generate_font_meta,
628
+ AssetType.TEXT: _generate_text_meta,
629
+ AssetType.PLUGIN: _generate_plugin_meta,
630
+ AssetType.MATERIAL: _generate_default_meta,
631
+ AssetType.PREFAB: _generate_default_meta,
632
+ AssetType.SCENE: _generate_default_meta,
633
+ AssetType.SCRIPTABLE_OBJECT: _generate_default_meta,
634
+ AssetType.ANIMATION: _generate_native_format_meta,
635
+ AssetType.DEFAULT: _generate_default_meta,
636
+ }
637
+
638
+
639
+ def generate_meta_content(
640
+ path: Path,
641
+ asset_type: AssetType | None = None,
642
+ options: MetaFileOptions | None = None,
643
+ ) -> str:
644
+ """Generate .meta file content for an asset.
645
+
646
+ Args:
647
+ path: Path to the asset (file or folder)
648
+ asset_type: Asset type to use (auto-detected if None)
649
+ options: Meta file generation options
650
+
651
+ Returns:
652
+ Meta file content as string
653
+ """
654
+ if options is None:
655
+ options = MetaFileOptions()
656
+
657
+ if asset_type is None:
658
+ asset_type = detect_asset_type(path)
659
+
660
+ # Generate or use provided GUID
661
+ if options.guid:
662
+ guid = options.guid
663
+ elif options.guid_seed:
664
+ guid = generate_guid(options.guid_seed)
665
+ else:
666
+ guid = generate_guid()
667
+
668
+ generator = META_GENERATORS.get(asset_type, _generate_default_meta)
669
+ return generator(guid, options)
670
+
671
+
672
+ def generate_meta_file(
673
+ path: Path,
674
+ asset_type: AssetType | None = None,
675
+ options: MetaFileOptions | None = None,
676
+ overwrite: bool = False,
677
+ ) -> Path:
678
+ """Generate a .meta file for an asset and write it to disk.
679
+
680
+ Args:
681
+ path: Path to the asset (file or folder)
682
+ asset_type: Asset type to use (auto-detected if None)
683
+ options: Meta file generation options
684
+ overwrite: Whether to overwrite existing .meta file
685
+
686
+ Returns:
687
+ Path to the generated .meta file
688
+
689
+ Raises:
690
+ FileExistsError: If meta file exists and overwrite is False
691
+ FileNotFoundError: If asset path doesn't exist
692
+ """
693
+ path = Path(path).resolve()
694
+
695
+ if not path.exists():
696
+ raise FileNotFoundError(f"Asset path does not exist: {path}")
697
+
698
+ meta_path = Path(str(path) + ".meta")
699
+
700
+ if meta_path.exists() and not overwrite:
701
+ raise FileExistsError(f"Meta file already exists: {meta_path}")
702
+
703
+ content = generate_meta_content(path, asset_type, options)
704
+ meta_path.write_text(content, encoding="utf-8", newline="\n")
705
+
706
+ return meta_path
707
+
708
+
709
+ def generate_meta_files_recursive(
710
+ directory: Path,
711
+ overwrite: bool = False,
712
+ skip_existing: bool = True,
713
+ options: MetaFileOptions | None = None,
714
+ progress_callback: callable | None = None,
715
+ ) -> list[tuple[Path, bool, str]]:
716
+ """Generate .meta files for all assets in a directory recursively.
717
+
718
+ Args:
719
+ directory: Directory to process
720
+ overwrite: Whether to overwrite existing .meta files
721
+ skip_existing: Skip files that already have .meta files (ignored if overwrite=True)
722
+ options: Base options for meta file generation
723
+ progress_callback: Optional callback for progress (current, total)
724
+
725
+ Returns:
726
+ List of (path, success, message) tuples
727
+ """
728
+ directory = Path(directory).resolve()
729
+
730
+ if not directory.is_dir():
731
+ raise NotADirectoryError(f"Not a directory: {directory}")
732
+
733
+ # Collect all files and folders that need .meta files
734
+ items_to_process: list[Path] = []
735
+
736
+ for item in directory.rglob("*"):
737
+ # Skip .meta files themselves
738
+ if item.suffix == ".meta":
739
+ continue
740
+
741
+ # Skip hidden files/folders
742
+ if any(part.startswith(".") for part in item.parts):
743
+ continue
744
+
745
+ meta_path = Path(str(item) + ".meta")
746
+
747
+ if meta_path.exists():
748
+ if overwrite:
749
+ items_to_process.append(item)
750
+ elif not skip_existing:
751
+ # Report as skipped
752
+ pass
753
+ else:
754
+ items_to_process.append(item)
755
+
756
+ # Also include the directory itself if it doesn't have a meta file
757
+ dir_meta = Path(str(directory) + ".meta")
758
+ if not dir_meta.exists() or overwrite:
759
+ items_to_process.insert(0, directory)
760
+
761
+ total = len(items_to_process)
762
+ results: list[tuple[Path, bool, str]] = []
763
+
764
+ for i, item in enumerate(items_to_process):
765
+ if progress_callback:
766
+ progress_callback(i + 1, total)
767
+
768
+ try:
769
+ generate_meta_file(item, options=options, overwrite=overwrite)
770
+ results.append((item, True, ""))
771
+ except Exception as e:
772
+ results.append((item, False, str(e)))
773
+
774
+ return results
775
+
776
+
777
+ def ensure_meta_file(
778
+ path: Path,
779
+ options: MetaFileOptions | None = None,
780
+ ) -> tuple[Path, bool]:
781
+ """Ensure an asset has a .meta file, creating one if needed.
782
+
783
+ Args:
784
+ path: Path to the asset
785
+ options: Meta file generation options
786
+
787
+ Returns:
788
+ Tuple of (meta_path, was_created)
789
+ """
790
+ path = Path(path).resolve()
791
+ meta_path = Path(str(path) + ".meta")
792
+
793
+ if meta_path.exists():
794
+ return meta_path, False
795
+
796
+ generate_meta_file(path, options=options, overwrite=False)
797
+ return meta_path, True
798
+
799
+
800
+ def get_guid_from_meta(meta_path: Path) -> str | None:
801
+ """Extract GUID from an existing .meta file.
802
+
803
+ Args:
804
+ meta_path: Path to the .meta file
805
+
806
+ Returns:
807
+ GUID string or None if not found
808
+ """
809
+ import re
810
+
811
+ pattern = re.compile(r"^guid:\s*([a-f0-9]{32})\s*$", re.MULTILINE)
812
+
813
+ try:
814
+ content = meta_path.read_text(encoding="utf-8")
815
+ match = pattern.search(content)
816
+ if match:
817
+ return match.group(1)
818
+ except OSError:
819
+ pass
820
+
821
+ return None
822
+
823
+
824
+ # ============================================================================
825
+ # Meta File Modification Functions
826
+ # ============================================================================
827
+
828
+
829
+ @dataclass
830
+ class MetaModification:
831
+ """Represents a modification to be applied to a meta file."""
832
+
833
+ field_path: str # Dot-separated path like "TextureImporter.spriteMode"
834
+ value: Any
835
+ create_if_missing: bool = False
836
+
837
+
838
+ def parse_meta_file(meta_path: Path) -> dict[str, Any]:
839
+ """Parse a .meta file into a dictionary structure.
840
+
841
+ Args:
842
+ meta_path: Path to the .meta file
843
+
844
+ Returns:
845
+ Dictionary representation of the meta file
846
+
847
+ Raises:
848
+ FileNotFoundError: If meta file doesn't exist
849
+ ValueError: If meta file is invalid
850
+ """
851
+
852
+ if not meta_path.exists():
853
+ raise FileNotFoundError(f"Meta file not found: {meta_path}")
854
+
855
+ content = meta_path.read_text(encoding="utf-8")
856
+ lines = content.split("\n")
857
+
858
+ result: dict[str, Any] = {}
859
+ stack: list[tuple[dict, int]] = [(result, -1)] # (current_dict, indent_level)
860
+
861
+ for line in lines:
862
+ if not line.strip() or line.startswith("%"):
863
+ continue
864
+
865
+ # Calculate indent level
866
+ stripped = line.lstrip()
867
+ indent = len(line) - len(stripped)
868
+
869
+ # Parse key-value
870
+ if ":" in stripped:
871
+ colon_idx = stripped.index(":")
872
+ key = stripped[:colon_idx].strip()
873
+ value_part = stripped[colon_idx + 1 :].strip()
874
+
875
+ # Pop stack until we find the right parent
876
+ while len(stack) > 1 and stack[-1][1] >= indent:
877
+ stack.pop()
878
+
879
+ current_dict = stack[-1][0]
880
+
881
+ if value_part == "" or value_part.startswith("#"):
882
+ # This is a nested object
883
+ new_dict: dict[str, Any] = {}
884
+ current_dict[key] = new_dict
885
+ stack.append((new_dict, indent))
886
+ elif value_part == "[]":
887
+ current_dict[key] = []
888
+ elif value_part == "{}":
889
+ current_dict[key] = {}
890
+ else:
891
+ # Parse the value
892
+ current_dict[key] = _parse_yaml_value(value_part)
893
+
894
+ return result
895
+
896
+
897
+ def _parse_yaml_value(value_str: str) -> Any:
898
+ """Parse a YAML value string into Python type."""
899
+ value_str = value_str.strip()
900
+
901
+ # Handle inline dict like {x: 0, y: 0}
902
+ if value_str.startswith("{") and value_str.endswith("}"):
903
+ inner = value_str[1:-1].strip()
904
+ if not inner:
905
+ return {}
906
+ result = {}
907
+ # Simple parsing for Unity's inline format
908
+ parts = inner.split(",")
909
+ for part in parts:
910
+ if ":" in part:
911
+ k, v = part.split(":", 1)
912
+ result[k.strip()] = _parse_yaml_value(v.strip())
913
+ return result
914
+
915
+ # Handle numbers
916
+ if value_str.isdigit() or (value_str.startswith("-") and value_str[1:].isdigit()):
917
+ return int(value_str)
918
+
919
+ try:
920
+ return float(value_str)
921
+ except ValueError:
922
+ pass
923
+
924
+ # Handle booleans
925
+ if value_str.lower() in ("true", "yes", "on"):
926
+ return True
927
+ if value_str.lower() in ("false", "no", "off"):
928
+ return False
929
+
930
+ # Handle quoted strings
931
+ if (value_str.startswith('"') and value_str.endswith('"')) or (
932
+ value_str.startswith("'") and value_str.endswith("'")
933
+ ):
934
+ return value_str[1:-1]
935
+
936
+ return value_str
937
+
938
+
939
+ def _serialize_yaml_value(value: Any, indent: int = 0) -> str:
940
+ """Serialize a Python value to YAML string."""
941
+ if isinstance(value, bool):
942
+ return "1" if value else "0"
943
+ elif isinstance(value, int):
944
+ return str(value)
945
+ elif isinstance(value, float):
946
+ # Format float without unnecessary trailing zeros
947
+ if value == int(value):
948
+ return str(int(value))
949
+ return str(value)
950
+ elif isinstance(value, dict):
951
+ if not value:
952
+ return "{}"
953
+ # Inline dict format for simple values
954
+ if all(isinstance(v, (int, float, str)) for v in value.values()):
955
+ parts = [f"{k}: {_serialize_yaml_value(v)}" for k, v in value.items()]
956
+ return "{" + ", ".join(parts) + "}"
957
+ # Multi-line dict
958
+ lines = []
959
+ for k, v in value.items():
960
+ lines.append(f"{' ' * indent}{k}: {_serialize_yaml_value(v, indent + 2)}")
961
+ return "\n" + "\n".join(lines)
962
+ elif isinstance(value, list):
963
+ if not value:
964
+ return "[]"
965
+ lines = []
966
+ for item in value:
967
+ lines.append(f"{' ' * indent}- {_serialize_yaml_value(item, indent + 2)}")
968
+ return "\n" + "\n".join(lines)
969
+ elif value is None:
970
+ return ""
971
+ else:
972
+ return str(value)
973
+
974
+
975
+ def modify_meta_file(
976
+ meta_path: Path,
977
+ modifications: dict[str, Any],
978
+ ) -> Path:
979
+ """Modify specific fields in an existing .meta file.
980
+
981
+ Note: GUID modification is not allowed to prevent breaking asset references.
982
+ Use generate-meta with --overwrite if you need to regenerate with a new GUID.
983
+
984
+ Args:
985
+ meta_path: Path to the .meta file
986
+ modifications: Dictionary of field paths to new values
987
+ Keys are dot-separated paths like "TextureImporter.spriteMode"
988
+
989
+ Returns:
990
+ Path to the modified .meta file
991
+
992
+ Raises:
993
+ FileNotFoundError: If meta file doesn't exist
994
+ ValueError: If attempting to modify GUID
995
+ """
996
+ meta_path = Path(meta_path)
997
+ if not meta_path.exists():
998
+ raise FileNotFoundError(f"Meta file not found: {meta_path}")
999
+
1000
+ # GUID modification is not allowed
1001
+ if "guid" in modifications:
1002
+ raise ValueError(
1003
+ "GUID modification is not allowed. "
1004
+ "Changing GUID will break all references to this asset. "
1005
+ "Use 'generate-meta --overwrite' if you need a new GUID."
1006
+ )
1007
+
1008
+ content = meta_path.read_text(encoding="utf-8")
1009
+
1010
+ # Apply modifications line by line for precise control
1011
+ lines = content.split("\n")
1012
+ modified_lines = _apply_modifications(lines, modifications)
1013
+
1014
+ new_content = "\n".join(modified_lines)
1015
+ meta_path.write_text(new_content, encoding="utf-8", newline="\n")
1016
+
1017
+ return meta_path
1018
+
1019
+
1020
+ def _apply_modifications(lines: list[str], modifications: dict[str, Any]) -> list[str]:
1021
+ """Apply modifications to meta file lines."""
1022
+
1023
+ result = lines.copy()
1024
+
1025
+ for field_path, value in modifications.items():
1026
+ parts = field_path.split(".")
1027
+ result = _modify_field(result, parts, value)
1028
+
1029
+ return result
1030
+
1031
+
1032
+ def _modify_field(lines: list[str], path_parts: list[str], value: Any) -> list[str]:
1033
+ """Modify a specific field in the lines."""
1034
+ result = lines.copy()
1035
+
1036
+ if len(path_parts) == 1:
1037
+ # Top-level field
1038
+ field_name = path_parts[0]
1039
+ for i, line in enumerate(result):
1040
+ stripped = line.lstrip()
1041
+ if stripped.startswith(f"{field_name}:"):
1042
+ indent = len(line) - len(stripped)
1043
+ result[i] = f"{' ' * indent}{field_name}: {_serialize_yaml_value(value)}"
1044
+ break
1045
+ else:
1046
+ # Nested field - find parent section first
1047
+ parent_path = path_parts[:-1]
1048
+ field_name = path_parts[-1]
1049
+
1050
+ current_path: list[str] = []
1051
+ indent_stack: list[int] = [-1]
1052
+
1053
+ for i, line in enumerate(result):
1054
+ if not line.strip():
1055
+ continue
1056
+
1057
+ stripped = line.lstrip()
1058
+ indent = len(line) - len(stripped)
1059
+
1060
+ # Pop stack for lower indents
1061
+ while indent_stack and indent <= indent_stack[-1]:
1062
+ indent_stack.pop()
1063
+ if current_path:
1064
+ current_path.pop()
1065
+
1066
+ if ":" in stripped:
1067
+ key = stripped.split(":")[0].strip()
1068
+ value_part = stripped.split(":", 1)[1].strip() if ":" in stripped else ""
1069
+
1070
+ if value_part == "" or value_part.startswith("#"):
1071
+ # Section header
1072
+ current_path.append(key)
1073
+ indent_stack.append(indent)
1074
+
1075
+ if current_path == parent_path:
1076
+ pass # Found parent section
1077
+ elif current_path == parent_path and key == field_name:
1078
+ # Found the field to modify
1079
+ result[i] = f"{' ' * indent}{field_name}: {_serialize_yaml_value(value)}"
1080
+ return result
1081
+
1082
+ # If we found the section but not the field, the field might need to be added
1083
+ # For now, just return as-is if not found
1084
+
1085
+ return result
1086
+
1087
+
1088
+ def set_texture_sprite_mode(
1089
+ meta_path: Path,
1090
+ sprite_mode: int = 1,
1091
+ pixels_per_unit: int | None = None,
1092
+ filter_mode: int | None = None,
1093
+ ) -> Path:
1094
+ """Set texture import settings for sprite mode.
1095
+
1096
+ Args:
1097
+ meta_path: Path to the texture .meta file
1098
+ sprite_mode: 0=None, 1=Single, 2=Multiple
1099
+ pixels_per_unit: Pixels per unit (default: keep existing)
1100
+ filter_mode: 0=Point, 1=Bilinear, 2=Trilinear (default: keep existing)
1101
+
1102
+ Returns:
1103
+ Path to the modified .meta file
1104
+ """
1105
+ meta_path = Path(meta_path)
1106
+ content = meta_path.read_text(encoding="utf-8")
1107
+ lines = content.split("\n")
1108
+
1109
+ modifications = []
1110
+
1111
+ # Set textureType to Sprite (8) if enabling sprite mode
1112
+ if sprite_mode > 0:
1113
+ modifications.append(("textureType", 8))
1114
+ modifications.append(("spriteMode", sprite_mode))
1115
+
1116
+ if pixels_per_unit is not None:
1117
+ modifications.append(("spritePixelsToUnits", pixels_per_unit))
1118
+
1119
+ if filter_mode is not None:
1120
+ modifications.append(("filterMode", filter_mode))
1121
+
1122
+ for field_name, value in modifications:
1123
+ for i, line in enumerate(lines):
1124
+ stripped = line.lstrip()
1125
+ if stripped.startswith(f"{field_name}:"):
1126
+ indent = len(line) - len(stripped)
1127
+ lines[i] = f"{' ' * indent}{field_name}: {value}"
1128
+ break
1129
+
1130
+ new_content = "\n".join(lines)
1131
+ meta_path.write_text(new_content, encoding="utf-8", newline="\n")
1132
+
1133
+ return meta_path
1134
+
1135
+
1136
+ def set_script_execution_order(meta_path: Path, execution_order: int) -> Path:
1137
+ """Set script execution order in a MonoImporter .meta file.
1138
+
1139
+ Args:
1140
+ meta_path: Path to the script .meta file
1141
+ execution_order: Execution order value (negative = earlier, positive = later)
1142
+
1143
+ Returns:
1144
+ Path to the modified .meta file
1145
+ """
1146
+ meta_path = Path(meta_path)
1147
+ content = meta_path.read_text(encoding="utf-8")
1148
+ lines = content.split("\n")
1149
+
1150
+ for i, line in enumerate(lines):
1151
+ stripped = line.lstrip()
1152
+ if stripped.startswith("executionOrder:"):
1153
+ indent = len(line) - len(stripped)
1154
+ lines[i] = f"{' ' * indent}executionOrder: {execution_order}"
1155
+ break
1156
+
1157
+ new_content = "\n".join(lines)
1158
+ meta_path.write_text(new_content, encoding="utf-8", newline="\n")
1159
+
1160
+ return meta_path
1161
+
1162
+
1163
+ def set_asset_bundle(
1164
+ meta_path: Path,
1165
+ bundle_name: str = "",
1166
+ bundle_variant: str = "",
1167
+ ) -> Path:
1168
+ """Set asset bundle name and variant in a .meta file.
1169
+
1170
+ Args:
1171
+ meta_path: Path to the .meta file
1172
+ bundle_name: Asset bundle name (empty string to clear)
1173
+ bundle_variant: Asset bundle variant (empty string to clear)
1174
+
1175
+ Returns:
1176
+ Path to the modified .meta file
1177
+ """
1178
+ meta_path = Path(meta_path)
1179
+ content = meta_path.read_text(encoding="utf-8")
1180
+ lines = content.split("\n")
1181
+
1182
+ for i, line in enumerate(lines):
1183
+ stripped = line.lstrip()
1184
+ if stripped.startswith("assetBundleName:"):
1185
+ indent = len(line) - len(stripped)
1186
+ lines[i] = f"{' ' * indent}assetBundleName: {bundle_name}"
1187
+ elif stripped.startswith("assetBundleVariant:"):
1188
+ indent = len(line) - len(stripped)
1189
+ lines[i] = f"{' ' * indent}assetBundleVariant: {bundle_variant}"
1190
+
1191
+ new_content = "\n".join(lines)
1192
+ meta_path.write_text(new_content, encoding="utf-8", newline="\n")
1193
+
1194
+ return meta_path
1195
+
1196
+
1197
+ def set_texture_max_size(meta_path: Path, max_size: int) -> Path:
1198
+ """Set maximum texture size in a TextureImporter .meta file.
1199
+
1200
+ Args:
1201
+ meta_path: Path to the texture .meta file
1202
+ max_size: Maximum texture size (32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384)
1203
+
1204
+ Returns:
1205
+ Path to the modified .meta file
1206
+ """
1207
+ valid_sizes = {32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384}
1208
+ if max_size not in valid_sizes:
1209
+ raise ValueError(f"Invalid max size: {max_size}. Must be one of {sorted(valid_sizes)}")
1210
+
1211
+ meta_path = Path(meta_path)
1212
+ content = meta_path.read_text(encoding="utf-8")
1213
+ lines = content.split("\n")
1214
+
1215
+ for i, line in enumerate(lines):
1216
+ stripped = line.lstrip()
1217
+ if stripped.startswith("maxTextureSize:"):
1218
+ indent = len(line) - len(stripped)
1219
+ lines[i] = f"{' ' * indent}maxTextureSize: {max_size}"
1220
+
1221
+ new_content = "\n".join(lines)
1222
+ meta_path.write_text(new_content, encoding="utf-8", newline="\n")
1223
+
1224
+ return meta_path
1225
+
1226
+
1227
+ def get_meta_info(meta_path: Path) -> dict[str, Any]:
1228
+ """Get summary information from a .meta file.
1229
+
1230
+ Args:
1231
+ meta_path: Path to the .meta file
1232
+
1233
+ Returns:
1234
+ Dictionary with meta file information
1235
+ """
1236
+ meta_path = Path(meta_path)
1237
+ if not meta_path.exists():
1238
+ raise FileNotFoundError(f"Meta file not found: {meta_path}")
1239
+
1240
+ content = meta_path.read_text(encoding="utf-8")
1241
+
1242
+ info: dict[str, Any] = {
1243
+ "path": str(meta_path),
1244
+ "guid": get_guid_from_meta(meta_path),
1245
+ "importer_type": None,
1246
+ "asset_bundle_name": None,
1247
+ "asset_bundle_variant": None,
1248
+ }
1249
+
1250
+ # Detect importer type
1251
+ importer_types = [
1252
+ "DefaultImporter",
1253
+ "MonoImporter",
1254
+ "TextureImporter",
1255
+ "AudioImporter",
1256
+ "VideoClipImporter",
1257
+ "ModelImporter",
1258
+ "ShaderImporter",
1259
+ "PluginImporter",
1260
+ "TrueTypeFontImporter",
1261
+ "TextScriptImporter",
1262
+ "NativeFormatImporter",
1263
+ ]
1264
+ for importer in importer_types:
1265
+ if f"{importer}:" in content:
1266
+ info["importer_type"] = importer
1267
+ break
1268
+
1269
+ # Extract specific fields
1270
+ for line in content.split("\n"):
1271
+ stripped = line.strip()
1272
+ if stripped.startswith("assetBundleName:"):
1273
+ value = stripped.split(":", 1)[1].strip()
1274
+ info["asset_bundle_name"] = value if value else None
1275
+ elif stripped.startswith("assetBundleVariant:"):
1276
+ value = stripped.split(":", 1)[1].strip()
1277
+ info["asset_bundle_variant"] = value if value else None
1278
+ elif stripped.startswith("spriteMode:"):
1279
+ info["sprite_mode"] = int(stripped.split(":")[1].strip())
1280
+ elif stripped.startswith("spritePixelsToUnits:"):
1281
+ info["pixels_per_unit"] = int(stripped.split(":")[1].strip())
1282
+ elif stripped.startswith("executionOrder:"):
1283
+ info["execution_order"] = int(stripped.split(":")[1].strip())
1284
+ elif stripped.startswith("maxTextureSize:") and "maxTextureSize" not in info:
1285
+ info["max_texture_size"] = int(stripped.split(":")[1].strip())
1286
+ elif stripped.startswith("textureType:"):
1287
+ info["texture_type"] = int(stripped.split(":")[1].strip())
1288
+ elif stripped.startswith("filterMode:"):
1289
+ info["filter_mode"] = int(stripped.split(":")[1].strip())
1290
+
1291
+ return info