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,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
|