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/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
+ )