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
unityflow/parser.py
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
"""Unity YAML Parser.
|
|
2
|
+
|
|
3
|
+
Handles Unity's custom YAML 1.1 dialect with:
|
|
4
|
+
- Custom tag namespace (!u! -> tag:unity3d.com,2011:)
|
|
5
|
+
- Multi-document files with !u!{ClassID} &{fileID} anchors
|
|
6
|
+
- Fast parsing using rapidyaml backend
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import random
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
from collections.abc import Iterator
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from unityflow.fast_parser import (
|
|
21
|
+
LARGE_FILE_THRESHOLD,
|
|
22
|
+
ProgressCallback,
|
|
23
|
+
fast_dump_unity_object,
|
|
24
|
+
fast_parse_unity_yaml,
|
|
25
|
+
get_file_stats,
|
|
26
|
+
iter_dump_unity_object,
|
|
27
|
+
iter_parse_unity_yaml,
|
|
28
|
+
stream_parse_unity_yaml_file,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Unity YAML header pattern
|
|
32
|
+
UNITY_HEADER = """%YAML 1.1
|
|
33
|
+
%TAG !u! tag:unity3d.com,2011:
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Pattern to match Unity document headers: --- !u!{ClassID} &{fileID}
|
|
37
|
+
# Note: fileID can be negative (Unity uses 64-bit signed integers)
|
|
38
|
+
DOCUMENT_HEADER_PATTERN = re.compile(r"^--- !u!(\d+) &(-?\d+)(?: stripped)?$", re.MULTILINE)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _load_class_ids() -> dict[int, str]:
|
|
42
|
+
"""Load Unity ClassIDs from JSON file.
|
|
43
|
+
|
|
44
|
+
The JSON file is generated from Unity's official ClassIDReference documentation.
|
|
45
|
+
Falls back to a minimal built-in set if the JSON file is not found.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Dictionary mapping class ID (int) to class name (str)
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
# Use importlib.resources for package data (Python 3.9+)
|
|
52
|
+
from importlib.resources import files
|
|
53
|
+
|
|
54
|
+
data_file = files("unityflow.data").joinpath("class_ids.json")
|
|
55
|
+
with data_file.open(encoding="utf-8") as f:
|
|
56
|
+
data = json.load(f)
|
|
57
|
+
# Convert string keys to int (JSON only supports string keys)
|
|
58
|
+
return {int(k): v for k, v in data.items()}
|
|
59
|
+
except (ImportError, FileNotFoundError, json.JSONDecodeError, OSError, ValueError, TypeError):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
# Fallback: minimal built-in set for essential types
|
|
63
|
+
return {
|
|
64
|
+
1: "GameObject",
|
|
65
|
+
4: "Transform",
|
|
66
|
+
114: "MonoBehaviour",
|
|
67
|
+
115: "MonoScript",
|
|
68
|
+
224: "RectTransform",
|
|
69
|
+
1001: "PrefabInstance",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Unity ClassIDs - loaded from data/class_ids.json
|
|
74
|
+
# Reference: https://docs.unity3d.com/Manual/ClassIDReference.html (Unity 6.3 LTS)
|
|
75
|
+
CLASS_IDS: dict[int, str] = _load_class_ids()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_parser_backend() -> str:
|
|
79
|
+
"""Get the current parser backend name."""
|
|
80
|
+
return "rapidyaml"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class UnityYAMLObject:
|
|
85
|
+
"""Represents a single Unity YAML document/object."""
|
|
86
|
+
|
|
87
|
+
class_id: int
|
|
88
|
+
file_id: int
|
|
89
|
+
data: dict[str, Any]
|
|
90
|
+
stripped: bool = False
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def class_name(self) -> str:
|
|
94
|
+
"""Get the human-readable class name for this object."""
|
|
95
|
+
return CLASS_IDS.get(self.class_id, f"Unknown({self.class_id})")
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def root_key(self) -> str | None:
|
|
99
|
+
"""Get the root key of the document (e.g., 'GameObject', 'Transform')."""
|
|
100
|
+
if self.data:
|
|
101
|
+
keys = list(self.data.keys())
|
|
102
|
+
return keys[0] if keys else None
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def get_content(self) -> dict[str, Any] | None:
|
|
106
|
+
"""Get the content under the root key."""
|
|
107
|
+
root = self.root_key
|
|
108
|
+
if root and root in self.data:
|
|
109
|
+
return self.data[root]
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def __repr__(self) -> str:
|
|
113
|
+
return f"UnityYAMLObject(class={self.class_name}, fileID={self.file_id})"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class UnityYAMLDocument:
|
|
118
|
+
"""Represents a complete Unity YAML file with multiple objects."""
|
|
119
|
+
|
|
120
|
+
objects: list[UnityYAMLObject] = field(default_factory=list)
|
|
121
|
+
source_path: Path | None = None
|
|
122
|
+
|
|
123
|
+
def __iter__(self) -> Iterator[UnityYAMLObject]:
|
|
124
|
+
return iter(self.objects)
|
|
125
|
+
|
|
126
|
+
def __len__(self) -> int:
|
|
127
|
+
return len(self.objects)
|
|
128
|
+
|
|
129
|
+
def get_by_file_id(self, file_id: int) -> UnityYAMLObject | None:
|
|
130
|
+
"""Find an object by its fileID."""
|
|
131
|
+
for obj in self.objects:
|
|
132
|
+
if obj.file_id == file_id:
|
|
133
|
+
return obj
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def get_by_class_id(self, class_id: int) -> list[UnityYAMLObject]:
|
|
137
|
+
"""Find all objects of a specific class type."""
|
|
138
|
+
return [obj for obj in self.objects if obj.class_id == class_id]
|
|
139
|
+
|
|
140
|
+
def get_game_objects(self) -> list[UnityYAMLObject]:
|
|
141
|
+
"""Get all GameObject objects."""
|
|
142
|
+
return self.get_by_class_id(1)
|
|
143
|
+
|
|
144
|
+
def get_transforms(self) -> list[UnityYAMLObject]:
|
|
145
|
+
"""Get all Transform objects."""
|
|
146
|
+
return self.get_by_class_id(4)
|
|
147
|
+
|
|
148
|
+
def get_prefab_instances(self) -> list[UnityYAMLObject]:
|
|
149
|
+
"""Get all PrefabInstance objects."""
|
|
150
|
+
return self.get_by_class_id(1001)
|
|
151
|
+
|
|
152
|
+
def get_rect_transforms(self) -> list[UnityYAMLObject]:
|
|
153
|
+
"""Get all RectTransform objects."""
|
|
154
|
+
return self.get_by_class_id(224)
|
|
155
|
+
|
|
156
|
+
def get_all_file_ids(self) -> set[int]:
|
|
157
|
+
"""Get all fileIDs in this document."""
|
|
158
|
+
return {obj.file_id for obj in self.objects}
|
|
159
|
+
|
|
160
|
+
def add_object(self, obj: UnityYAMLObject) -> None:
|
|
161
|
+
"""Add a new object to the document.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
obj: The UnityYAMLObject to add
|
|
165
|
+
"""
|
|
166
|
+
self.objects.append(obj)
|
|
167
|
+
|
|
168
|
+
def remove_object(self, file_id: int) -> bool:
|
|
169
|
+
"""Remove an object by its fileID.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
file_id: The fileID of the object to remove
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
True if removed, False if not found
|
|
176
|
+
"""
|
|
177
|
+
for i, obj in enumerate(self.objects):
|
|
178
|
+
if obj.file_id == file_id:
|
|
179
|
+
self.objects.pop(i)
|
|
180
|
+
return True
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
def generate_unique_file_id(self) -> int:
|
|
184
|
+
"""Generate a unique fileID for this document.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
A fileID that doesn't conflict with existing objects
|
|
188
|
+
"""
|
|
189
|
+
existing = self.get_all_file_ids()
|
|
190
|
+
return generate_file_id(existing)
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def load(
|
|
194
|
+
cls,
|
|
195
|
+
path: str | Path,
|
|
196
|
+
progress_callback: ProgressCallback | None = None,
|
|
197
|
+
) -> UnityYAMLDocument:
|
|
198
|
+
"""Load a Unity YAML file from disk.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
path: Path to the Unity YAML file
|
|
202
|
+
progress_callback: Optional callback for progress reporting
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Parsed UnityYAMLDocument
|
|
206
|
+
"""
|
|
207
|
+
path = Path(path)
|
|
208
|
+
content = path.read_text(encoding="utf-8")
|
|
209
|
+
doc = cls.parse(content, progress_callback)
|
|
210
|
+
doc.source_path = path
|
|
211
|
+
return doc
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def load_streaming(
|
|
215
|
+
cls,
|
|
216
|
+
path: str | Path,
|
|
217
|
+
progress_callback: ProgressCallback | None = None,
|
|
218
|
+
) -> UnityYAMLDocument:
|
|
219
|
+
"""Load a large Unity YAML file using streaming mode.
|
|
220
|
+
|
|
221
|
+
This method is optimized for large files (10MB+) and uses less memory
|
|
222
|
+
by processing the file in chunks.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
path: Path to the Unity YAML file
|
|
226
|
+
progress_callback: Optional callback for progress reporting (bytes_read, total_bytes)
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Parsed UnityYAMLDocument
|
|
230
|
+
"""
|
|
231
|
+
path = Path(path)
|
|
232
|
+
doc = cls()
|
|
233
|
+
doc.source_path = path
|
|
234
|
+
|
|
235
|
+
for class_id, file_id, stripped, data in stream_parse_unity_yaml_file(
|
|
236
|
+
path, progress_callback=progress_callback
|
|
237
|
+
):
|
|
238
|
+
obj = UnityYAMLObject(
|
|
239
|
+
class_id=class_id,
|
|
240
|
+
file_id=file_id,
|
|
241
|
+
data=data,
|
|
242
|
+
stripped=stripped,
|
|
243
|
+
)
|
|
244
|
+
doc.objects.append(obj)
|
|
245
|
+
|
|
246
|
+
return doc
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def load_auto(
|
|
250
|
+
cls,
|
|
251
|
+
path: str | Path,
|
|
252
|
+
progress_callback: ProgressCallback | None = None,
|
|
253
|
+
) -> UnityYAMLDocument:
|
|
254
|
+
"""Load a Unity YAML file, automatically choosing the best method.
|
|
255
|
+
|
|
256
|
+
For files smaller than 10MB, uses the standard load method.
|
|
257
|
+
For larger files, uses streaming mode for better memory efficiency.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
path: Path to the Unity YAML file
|
|
261
|
+
progress_callback: Optional callback for progress reporting
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Parsed UnityYAMLDocument
|
|
265
|
+
"""
|
|
266
|
+
path = Path(path)
|
|
267
|
+
file_size = path.stat().st_size
|
|
268
|
+
|
|
269
|
+
if file_size >= LARGE_FILE_THRESHOLD:
|
|
270
|
+
return cls.load_streaming(path, progress_callback)
|
|
271
|
+
else:
|
|
272
|
+
return cls.load(path, progress_callback)
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def parse(
|
|
276
|
+
cls,
|
|
277
|
+
content: str,
|
|
278
|
+
progress_callback: ProgressCallback | None = None,
|
|
279
|
+
) -> UnityYAMLDocument:
|
|
280
|
+
"""Parse Unity YAML content from a string.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
content: Unity YAML content string
|
|
284
|
+
progress_callback: Optional callback for progress reporting
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Parsed UnityYAMLDocument
|
|
288
|
+
"""
|
|
289
|
+
doc = cls()
|
|
290
|
+
|
|
291
|
+
parsed = fast_parse_unity_yaml(content, progress_callback)
|
|
292
|
+
|
|
293
|
+
for class_id, file_id, stripped, data in parsed:
|
|
294
|
+
obj = UnityYAMLObject(
|
|
295
|
+
class_id=class_id,
|
|
296
|
+
file_id=file_id,
|
|
297
|
+
data=data,
|
|
298
|
+
stripped=stripped,
|
|
299
|
+
)
|
|
300
|
+
doc.objects.append(obj)
|
|
301
|
+
|
|
302
|
+
return doc
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
def iter_parse(
|
|
306
|
+
cls,
|
|
307
|
+
content: str,
|
|
308
|
+
progress_callback: ProgressCallback | None = None,
|
|
309
|
+
) -> Iterator[UnityYAMLObject]:
|
|
310
|
+
"""Parse Unity YAML content, yielding objects one at a time.
|
|
311
|
+
|
|
312
|
+
This is a memory-efficient generator version for processing large content.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
content: Unity YAML content string
|
|
316
|
+
progress_callback: Optional callback for progress reporting
|
|
317
|
+
|
|
318
|
+
Yields:
|
|
319
|
+
UnityYAMLObject instances
|
|
320
|
+
"""
|
|
321
|
+
for class_id, file_id, stripped, data in iter_parse_unity_yaml(content, progress_callback):
|
|
322
|
+
yield UnityYAMLObject(
|
|
323
|
+
class_id=class_id,
|
|
324
|
+
file_id=file_id,
|
|
325
|
+
data=data,
|
|
326
|
+
stripped=stripped,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def dump(self) -> str:
|
|
330
|
+
"""Serialize the document back to Unity YAML format."""
|
|
331
|
+
output_lines = [UNITY_HEADER.rstrip()]
|
|
332
|
+
|
|
333
|
+
for obj in self.objects:
|
|
334
|
+
# Write document header
|
|
335
|
+
header = f"--- !u!{obj.class_id} &{obj.file_id}"
|
|
336
|
+
if obj.stripped:
|
|
337
|
+
header += " stripped"
|
|
338
|
+
output_lines.append(header)
|
|
339
|
+
|
|
340
|
+
# Serialize document content
|
|
341
|
+
if obj.data:
|
|
342
|
+
content = fast_dump_unity_object(obj.data)
|
|
343
|
+
if content:
|
|
344
|
+
output_lines.append(content)
|
|
345
|
+
|
|
346
|
+
# Unity uses LF line endings
|
|
347
|
+
return "\n".join(output_lines) + "\n"
|
|
348
|
+
|
|
349
|
+
def iter_dump(self) -> Iterator[str]:
|
|
350
|
+
"""Serialize the document, yielding lines one at a time.
|
|
351
|
+
|
|
352
|
+
This is a memory-efficient generator version for large documents.
|
|
353
|
+
|
|
354
|
+
Yields:
|
|
355
|
+
YAML lines as strings
|
|
356
|
+
"""
|
|
357
|
+
yield UNITY_HEADER.rstrip()
|
|
358
|
+
|
|
359
|
+
for obj in self.objects:
|
|
360
|
+
# Write document header
|
|
361
|
+
header = f"--- !u!{obj.class_id} &{obj.file_id}"
|
|
362
|
+
if obj.stripped:
|
|
363
|
+
header += " stripped"
|
|
364
|
+
yield header
|
|
365
|
+
|
|
366
|
+
# Serialize document content
|
|
367
|
+
if obj.data:
|
|
368
|
+
yield from iter_dump_unity_object(obj.data)
|
|
369
|
+
|
|
370
|
+
def save(self, path: str | Path) -> None:
|
|
371
|
+
"""Save the document to a file."""
|
|
372
|
+
path = Path(path)
|
|
373
|
+
content = self.dump()
|
|
374
|
+
path.write_text(content, encoding="utf-8", newline="\n")
|
|
375
|
+
|
|
376
|
+
def save_streaming(self, path: str | Path) -> None:
|
|
377
|
+
"""Save the document to a file using streaming mode.
|
|
378
|
+
|
|
379
|
+
This is more memory-efficient for large documents as it writes
|
|
380
|
+
line by line instead of building the entire content in memory.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
path: Output file path
|
|
384
|
+
"""
|
|
385
|
+
path = Path(path)
|
|
386
|
+
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
|
387
|
+
for line in self.iter_dump():
|
|
388
|
+
f.write(line)
|
|
389
|
+
f.write("\n")
|
|
390
|
+
|
|
391
|
+
@staticmethod
|
|
392
|
+
def get_stats(path: str | Path) -> dict[str, Any]:
|
|
393
|
+
"""Get statistics about a Unity YAML file without fully parsing it.
|
|
394
|
+
|
|
395
|
+
This is a fast operation that only scans document headers.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
path: Path to the Unity YAML file
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Dictionary with file statistics including:
|
|
402
|
+
- file_size: Size in bytes
|
|
403
|
+
- file_size_mb: Size in megabytes
|
|
404
|
+
- document_count: Number of YAML documents
|
|
405
|
+
- class_counts: Count of each class type
|
|
406
|
+
- is_large_file: Whether the file exceeds the large file threshold
|
|
407
|
+
"""
|
|
408
|
+
return get_file_stats(path)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def parse_file_reference(ref: dict[str, Any] | None) -> tuple[int, str | None, int | None] | None:
|
|
412
|
+
"""Parse a Unity file reference.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
ref: A dictionary with fileID, optional guid, and optional type
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Tuple of (fileID, guid, type) or None if invalid
|
|
419
|
+
"""
|
|
420
|
+
if ref is None:
|
|
421
|
+
return None
|
|
422
|
+
if not isinstance(ref, dict):
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
file_id = ref.get("fileID")
|
|
426
|
+
if file_id is None:
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
guid = ref.get("guid")
|
|
430
|
+
ref_type = ref.get("type")
|
|
431
|
+
|
|
432
|
+
return (int(file_id), guid, ref_type)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def create_file_reference(
|
|
436
|
+
file_id: int,
|
|
437
|
+
guid: str | None = None,
|
|
438
|
+
ref_type: int | None = None,
|
|
439
|
+
) -> dict[str, Any]:
|
|
440
|
+
"""Create a Unity file reference.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
file_id: The local file ID
|
|
444
|
+
guid: Optional GUID for external references
|
|
445
|
+
ref_type: Optional type (usually 2 for assets, 3 for scripts)
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Dictionary with the reference
|
|
449
|
+
"""
|
|
450
|
+
ref: dict[str, Any] = {"fileID": file_id}
|
|
451
|
+
if guid is not None:
|
|
452
|
+
ref["guid"] = guid
|
|
453
|
+
if ref_type is not None:
|
|
454
|
+
ref["type"] = ref_type
|
|
455
|
+
return ref
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# Global counter for fileID generation (ensures uniqueness within a session)
|
|
459
|
+
_file_id_counter = 0
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def generate_file_id(existing_ids: set[int] | None = None) -> int:
|
|
463
|
+
"""Generate a unique fileID for a new Unity object.
|
|
464
|
+
|
|
465
|
+
Unity uses large integers for fileIDs. This function generates IDs
|
|
466
|
+
that are unique and follow Unity's conventions.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
existing_ids: Optional set of existing fileIDs to avoid collisions
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
A unique fileID (large positive integer)
|
|
473
|
+
"""
|
|
474
|
+
global _file_id_counter
|
|
475
|
+
_file_id_counter += 1
|
|
476
|
+
|
|
477
|
+
# Generate a unique ID based on timestamp + counter + random
|
|
478
|
+
# Unity typically uses large numbers (10+ digits)
|
|
479
|
+
timestamp_part = int(time.time() * 1000) % 10000000000
|
|
480
|
+
random_part = random.randint(1000000, 9999999)
|
|
481
|
+
file_id = timestamp_part * 10000000 + random_part + _file_id_counter
|
|
482
|
+
|
|
483
|
+
# Ensure uniqueness if existing_ids provided
|
|
484
|
+
if existing_ids:
|
|
485
|
+
while file_id in existing_ids:
|
|
486
|
+
_file_id_counter += 1
|
|
487
|
+
random_part = random.randint(1000000, 9999999)
|
|
488
|
+
file_id = timestamp_part * 10000000 + random_part + _file_id_counter
|
|
489
|
+
|
|
490
|
+
return file_id
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def create_game_object(
|
|
494
|
+
name: str,
|
|
495
|
+
file_id: int | None = None,
|
|
496
|
+
layer: int = 0,
|
|
497
|
+
tag: str = "Untagged",
|
|
498
|
+
is_active: bool = True,
|
|
499
|
+
components: list[int] | None = None,
|
|
500
|
+
) -> UnityYAMLObject:
|
|
501
|
+
"""Create a new GameObject object.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
name: Name of the GameObject
|
|
505
|
+
file_id: Optional fileID (generated if not provided)
|
|
506
|
+
layer: Layer number (default: 0)
|
|
507
|
+
tag: Tag string (default: "Untagged")
|
|
508
|
+
is_active: Whether the object is active (default: True)
|
|
509
|
+
components: List of component fileIDs
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
UnityYAMLObject representing the GameObject
|
|
513
|
+
"""
|
|
514
|
+
if file_id is None:
|
|
515
|
+
file_id = generate_file_id()
|
|
516
|
+
|
|
517
|
+
content = {
|
|
518
|
+
"m_ObjectHideFlags": 0,
|
|
519
|
+
"m_CorrespondingSourceObject": {"fileID": 0},
|
|
520
|
+
"m_PrefabInstance": {"fileID": 0},
|
|
521
|
+
"m_PrefabAsset": {"fileID": 0},
|
|
522
|
+
"serializedVersion": 6,
|
|
523
|
+
"m_Component": [{"component": {"fileID": c}} for c in (components or [])],
|
|
524
|
+
"m_Layer": layer,
|
|
525
|
+
"m_Name": name,
|
|
526
|
+
"m_TagString": tag,
|
|
527
|
+
"m_Icon": {"fileID": 0},
|
|
528
|
+
"m_NavMeshLayer": 0,
|
|
529
|
+
"m_StaticEditorFlags": 0,
|
|
530
|
+
"m_IsActive": 1 if is_active else 0,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return UnityYAMLObject(
|
|
534
|
+
class_id=1,
|
|
535
|
+
file_id=file_id,
|
|
536
|
+
data={"GameObject": content},
|
|
537
|
+
stripped=False,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def create_transform(
|
|
542
|
+
game_object_id: int,
|
|
543
|
+
file_id: int | None = None,
|
|
544
|
+
position: dict[str, float] | None = None,
|
|
545
|
+
rotation: dict[str, float] | None = None,
|
|
546
|
+
scale: dict[str, float] | None = None,
|
|
547
|
+
parent_id: int = 0,
|
|
548
|
+
children_ids: list[int] | None = None,
|
|
549
|
+
) -> UnityYAMLObject:
|
|
550
|
+
"""Create a new Transform component.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
game_object_id: fileID of the parent GameObject
|
|
554
|
+
file_id: Optional fileID (generated if not provided)
|
|
555
|
+
position: Local position {x, y, z} (default: origin)
|
|
556
|
+
rotation: Local rotation quaternion {x, y, z, w} (default: identity)
|
|
557
|
+
scale: Local scale {x, y, z} (default: 1,1,1)
|
|
558
|
+
parent_id: fileID of parent Transform (0 for root)
|
|
559
|
+
children_ids: List of children Transform fileIDs
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
UnityYAMLObject representing the Transform
|
|
563
|
+
"""
|
|
564
|
+
if file_id is None:
|
|
565
|
+
file_id = generate_file_id()
|
|
566
|
+
|
|
567
|
+
content = {
|
|
568
|
+
"m_ObjectHideFlags": 0,
|
|
569
|
+
"m_CorrespondingSourceObject": {"fileID": 0},
|
|
570
|
+
"m_PrefabInstance": {"fileID": 0},
|
|
571
|
+
"m_PrefabAsset": {"fileID": 0},
|
|
572
|
+
"m_GameObject": {"fileID": game_object_id},
|
|
573
|
+
"serializedVersion": 2,
|
|
574
|
+
"m_LocalRotation": rotation or {"x": 0, "y": 0, "z": 0, "w": 1},
|
|
575
|
+
"m_LocalPosition": position or {"x": 0, "y": 0, "z": 0},
|
|
576
|
+
"m_LocalScale": scale or {"x": 1, "y": 1, "z": 1},
|
|
577
|
+
"m_ConstrainProportionsScale": 0,
|
|
578
|
+
"m_Children": [{"fileID": c} for c in (children_ids or [])],
|
|
579
|
+
"m_Father": {"fileID": parent_id},
|
|
580
|
+
"m_LocalEulerAnglesHint": {"x": 0, "y": 0, "z": 0},
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return UnityYAMLObject(
|
|
584
|
+
class_id=4,
|
|
585
|
+
file_id=file_id,
|
|
586
|
+
data={"Transform": content},
|
|
587
|
+
stripped=False,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def create_rect_transform(
|
|
592
|
+
game_object_id: int,
|
|
593
|
+
file_id: int | None = None,
|
|
594
|
+
position: dict[str, float] | None = None,
|
|
595
|
+
rotation: dict[str, float] | None = None,
|
|
596
|
+
scale: dict[str, float] | None = None,
|
|
597
|
+
parent_id: int = 0,
|
|
598
|
+
children_ids: list[int] | None = None,
|
|
599
|
+
anchor_min: dict[str, float] | None = None,
|
|
600
|
+
anchor_max: dict[str, float] | None = None,
|
|
601
|
+
anchored_position: dict[str, float] | None = None,
|
|
602
|
+
size_delta: dict[str, float] | None = None,
|
|
603
|
+
pivot: dict[str, float] | None = None,
|
|
604
|
+
) -> UnityYAMLObject:
|
|
605
|
+
"""Create a new RectTransform component for UI elements.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
game_object_id: fileID of the parent GameObject
|
|
609
|
+
file_id: Optional fileID (generated if not provided)
|
|
610
|
+
position: Local position {x, y, z} (default: origin)
|
|
611
|
+
rotation: Local rotation quaternion {x, y, z, w} (default: identity)
|
|
612
|
+
scale: Local scale {x, y, z} (default: 1,1,1)
|
|
613
|
+
parent_id: fileID of parent RectTransform (0 for root)
|
|
614
|
+
children_ids: List of children RectTransform fileIDs
|
|
615
|
+
anchor_min: Anchor min point {x, y} (default: {0.5, 0.5})
|
|
616
|
+
anchor_max: Anchor max point {x, y} (default: {0.5, 0.5})
|
|
617
|
+
anchored_position: Position relative to anchors {x, y} (default: origin)
|
|
618
|
+
size_delta: Size delta from anchored rect {x, y} (default: {100, 100})
|
|
619
|
+
pivot: Pivot point {x, y} (default: center {0.5, 0.5})
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
UnityYAMLObject representing the RectTransform
|
|
623
|
+
"""
|
|
624
|
+
if file_id is None:
|
|
625
|
+
file_id = generate_file_id()
|
|
626
|
+
|
|
627
|
+
content = {
|
|
628
|
+
"m_ObjectHideFlags": 0,
|
|
629
|
+
"m_CorrespondingSourceObject": {"fileID": 0},
|
|
630
|
+
"m_PrefabInstance": {"fileID": 0},
|
|
631
|
+
"m_PrefabAsset": {"fileID": 0},
|
|
632
|
+
"m_GameObject": {"fileID": game_object_id},
|
|
633
|
+
"m_LocalRotation": rotation or {"x": 0, "y": 0, "z": 0, "w": 1},
|
|
634
|
+
"m_LocalPosition": position or {"x": 0, "y": 0, "z": 0},
|
|
635
|
+
"m_LocalScale": scale or {"x": 1, "y": 1, "z": 1},
|
|
636
|
+
"m_ConstrainProportionsScale": 0,
|
|
637
|
+
"m_Children": [{"fileID": c} for c in (children_ids or [])],
|
|
638
|
+
"m_Father": {"fileID": parent_id},
|
|
639
|
+
"m_LocalEulerAnglesHint": {"x": 0, "y": 0, "z": 0},
|
|
640
|
+
"m_AnchorMin": anchor_min or {"x": 0.5, "y": 0.5},
|
|
641
|
+
"m_AnchorMax": anchor_max or {"x": 0.5, "y": 0.5},
|
|
642
|
+
"m_AnchoredPosition": anchored_position or {"x": 0, "y": 0},
|
|
643
|
+
"m_SizeDelta": size_delta or {"x": 100, "y": 100},
|
|
644
|
+
"m_Pivot": pivot or {"x": 0.5, "y": 0.5},
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return UnityYAMLObject(
|
|
648
|
+
class_id=224,
|
|
649
|
+
file_id=file_id,
|
|
650
|
+
data={"RectTransform": content},
|
|
651
|
+
stripped=False,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def create_mono_behaviour(
|
|
656
|
+
game_object_id: int,
|
|
657
|
+
script_guid: str,
|
|
658
|
+
file_id: int | None = None,
|
|
659
|
+
enabled: bool = True,
|
|
660
|
+
properties: dict[str, Any] | None = None,
|
|
661
|
+
) -> UnityYAMLObject:
|
|
662
|
+
"""Create a new MonoBehaviour component.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
game_object_id: fileID of the parent GameObject
|
|
666
|
+
script_guid: GUID of the script asset
|
|
667
|
+
file_id: Optional fileID (generated if not provided)
|
|
668
|
+
enabled: Whether the component is enabled (default: True)
|
|
669
|
+
properties: Custom serialized fields
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
UnityYAMLObject representing the MonoBehaviour
|
|
673
|
+
"""
|
|
674
|
+
if file_id is None:
|
|
675
|
+
file_id = generate_file_id()
|
|
676
|
+
|
|
677
|
+
content = {
|
|
678
|
+
"m_ObjectHideFlags": 0,
|
|
679
|
+
"m_CorrespondingSourceObject": {"fileID": 0},
|
|
680
|
+
"m_PrefabInstance": {"fileID": 0},
|
|
681
|
+
"m_PrefabAsset": {"fileID": 0},
|
|
682
|
+
"m_GameObject": {"fileID": game_object_id},
|
|
683
|
+
"m_Enabled": 1 if enabled else 0,
|
|
684
|
+
"m_EditorHideFlags": 0,
|
|
685
|
+
"m_Script": {"fileID": 11500000, "guid": script_guid, "type": 3},
|
|
686
|
+
"m_EditorClassIdentifier": "",
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
# Add custom properties
|
|
690
|
+
if properties:
|
|
691
|
+
content.update(properties)
|
|
692
|
+
|
|
693
|
+
return UnityYAMLObject(
|
|
694
|
+
class_id=114,
|
|
695
|
+
file_id=file_id,
|
|
696
|
+
data={"MonoBehaviour": content},
|
|
697
|
+
stripped=False,
|
|
698
|
+
)
|