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/query.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Path-based query and surgical editing for Unity YAML files.
|
|
2
|
+
|
|
3
|
+
Provides JSONPath-like querying and modification of Unity prefab data.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from unityflow.formats import export_to_json
|
|
12
|
+
from unityflow.parser import UnityYAMLDocument
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class QueryResult:
|
|
17
|
+
"""Result of a path-based query."""
|
|
18
|
+
|
|
19
|
+
path: str
|
|
20
|
+
value: Any
|
|
21
|
+
file_id: int | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def query_path(doc: UnityYAMLDocument, path: str) -> list[QueryResult]:
|
|
25
|
+
"""Query a Unity YAML document using a path expression.
|
|
26
|
+
|
|
27
|
+
Path syntax:
|
|
28
|
+
- gameObjects/*/name - all GameObject names
|
|
29
|
+
- gameObjects/12345/name - specific GameObject's name
|
|
30
|
+
- components/*/type - all component types
|
|
31
|
+
- components/12345/localPosition - specific component's position
|
|
32
|
+
- **/m_Name - recursive search for m_Name
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
doc: The parsed Unity YAML document
|
|
36
|
+
path: The path expression to query
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List of QueryResult objects
|
|
40
|
+
"""
|
|
41
|
+
# Export to JSON structure for easier querying
|
|
42
|
+
prefab_json = export_to_json(doc, include_raw=False)
|
|
43
|
+
json_data = prefab_json.to_dict()
|
|
44
|
+
|
|
45
|
+
results: list[QueryResult] = []
|
|
46
|
+
|
|
47
|
+
# Parse path
|
|
48
|
+
parts = path.split("/")
|
|
49
|
+
|
|
50
|
+
if not parts:
|
|
51
|
+
return results
|
|
52
|
+
|
|
53
|
+
# Handle different root paths
|
|
54
|
+
root = parts[0]
|
|
55
|
+
rest = parts[1:] if len(parts) > 1 else []
|
|
56
|
+
|
|
57
|
+
if root == "gameObjects":
|
|
58
|
+
_query_objects(prefab_json.game_objects, rest, "gameObjects", results)
|
|
59
|
+
elif root == "components":
|
|
60
|
+
_query_objects(prefab_json.components, rest, "components", results)
|
|
61
|
+
elif root == "**":
|
|
62
|
+
# Recursive search
|
|
63
|
+
_query_recursive(json_data, rest, "", results)
|
|
64
|
+
else:
|
|
65
|
+
# Try direct path on full data
|
|
66
|
+
_query_objects(json_data, parts, "", results)
|
|
67
|
+
|
|
68
|
+
return results
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _query_objects(
|
|
72
|
+
objects: dict[str, Any],
|
|
73
|
+
path_parts: list[str],
|
|
74
|
+
prefix: str,
|
|
75
|
+
results: list[QueryResult],
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Query a dictionary of objects."""
|
|
78
|
+
if not path_parts:
|
|
79
|
+
# Return all objects
|
|
80
|
+
for key, value in objects.items():
|
|
81
|
+
results.append(
|
|
82
|
+
QueryResult(
|
|
83
|
+
path=f"{prefix}/{key}" if prefix else key,
|
|
84
|
+
value=value,
|
|
85
|
+
file_id=int(key) if key.isdigit() else None,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
selector = path_parts[0]
|
|
91
|
+
rest = path_parts[1:]
|
|
92
|
+
|
|
93
|
+
if selector == "*":
|
|
94
|
+
# Wildcard - query all objects
|
|
95
|
+
for key, obj in objects.items():
|
|
96
|
+
if isinstance(obj, dict):
|
|
97
|
+
_query_value(obj, rest, f"{prefix}/{key}" if prefix else key, results)
|
|
98
|
+
else:
|
|
99
|
+
if not rest:
|
|
100
|
+
results.append(
|
|
101
|
+
QueryResult(
|
|
102
|
+
path=f"{prefix}/{key}" if prefix else key,
|
|
103
|
+
value=obj,
|
|
104
|
+
file_id=int(key) if key.isdigit() else None,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
elif selector in objects:
|
|
108
|
+
# Specific key
|
|
109
|
+
obj = objects[selector]
|
|
110
|
+
if isinstance(obj, dict):
|
|
111
|
+
_query_value(obj, rest, f"{prefix}/{selector}" if prefix else selector, results)
|
|
112
|
+
else:
|
|
113
|
+
if not rest:
|
|
114
|
+
results.append(
|
|
115
|
+
QueryResult(
|
|
116
|
+
path=f"{prefix}/{selector}" if prefix else selector,
|
|
117
|
+
value=obj,
|
|
118
|
+
file_id=int(selector) if selector.isdigit() else None,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _query_value(
|
|
124
|
+
value: Any,
|
|
125
|
+
path_parts: list[str],
|
|
126
|
+
current_path: str,
|
|
127
|
+
results: list[QueryResult],
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Query a value at a given path."""
|
|
130
|
+
if not path_parts:
|
|
131
|
+
results.append(QueryResult(path=current_path, value=value))
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
key = path_parts[0]
|
|
135
|
+
rest = path_parts[1:]
|
|
136
|
+
|
|
137
|
+
if isinstance(value, dict):
|
|
138
|
+
if key == "*":
|
|
139
|
+
for k, v in value.items():
|
|
140
|
+
_query_value(v, rest, f"{current_path}/{k}", results)
|
|
141
|
+
elif key in value:
|
|
142
|
+
_query_value(value[key], rest, f"{current_path}/{key}", results)
|
|
143
|
+
elif isinstance(value, list):
|
|
144
|
+
if key == "*":
|
|
145
|
+
for i, item in enumerate(value):
|
|
146
|
+
_query_value(item, rest, f"{current_path}[{i}]", results)
|
|
147
|
+
elif key.isdigit():
|
|
148
|
+
idx = int(key)
|
|
149
|
+
if 0 <= idx < len(value):
|
|
150
|
+
_query_value(value[idx], rest, f"{current_path}[{idx}]", results)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _query_recursive(
|
|
154
|
+
value: Any,
|
|
155
|
+
path_parts: list[str],
|
|
156
|
+
current_path: str,
|
|
157
|
+
results: list[QueryResult],
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Recursively search for a path pattern."""
|
|
160
|
+
if not path_parts:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
target = path_parts[0]
|
|
164
|
+
rest = path_parts[1:]
|
|
165
|
+
|
|
166
|
+
if isinstance(value, dict):
|
|
167
|
+
for key, val in value.items():
|
|
168
|
+
new_path = f"{current_path}/{key}" if current_path else key
|
|
169
|
+
|
|
170
|
+
# Check if this key matches
|
|
171
|
+
if key == target or (target == "*" and True):
|
|
172
|
+
if rest:
|
|
173
|
+
_query_value(val, rest, new_path, results)
|
|
174
|
+
else:
|
|
175
|
+
results.append(QueryResult(path=new_path, value=val))
|
|
176
|
+
|
|
177
|
+
# Continue recursive search
|
|
178
|
+
_query_recursive(val, path_parts, new_path, results)
|
|
179
|
+
|
|
180
|
+
elif isinstance(value, list):
|
|
181
|
+
for i, item in enumerate(value):
|
|
182
|
+
new_path = f"{current_path}[{i}]"
|
|
183
|
+
_query_recursive(item, path_parts, new_path, results)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def set_value(
|
|
187
|
+
doc: UnityYAMLDocument,
|
|
188
|
+
path: str,
|
|
189
|
+
value: Any,
|
|
190
|
+
*,
|
|
191
|
+
create: bool = False,
|
|
192
|
+
) -> bool:
|
|
193
|
+
"""Set a value at a specific path in the document.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
doc: The Unity YAML document to modify
|
|
197
|
+
path: The path to the value (e.g., "components/12345/localPosition")
|
|
198
|
+
value: The new value to set
|
|
199
|
+
create: If True, create the path if it doesn't exist
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
True if the value was set, False if path not found
|
|
203
|
+
|
|
204
|
+
Note:
|
|
205
|
+
When creating new fields, they are appended at the end. Unity will
|
|
206
|
+
reorder fields according to the C# script declaration order when
|
|
207
|
+
the file is saved in the editor.
|
|
208
|
+
"""
|
|
209
|
+
parts = path.split("/")
|
|
210
|
+
|
|
211
|
+
if len(parts) < 2:
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
file_id_str = parts[1]
|
|
215
|
+
property_path = parts[2:] if len(parts) > 2 else []
|
|
216
|
+
|
|
217
|
+
# Find the object
|
|
218
|
+
if not file_id_str.isdigit():
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
file_id = int(file_id_str)
|
|
222
|
+
obj = doc.get_by_file_id(file_id)
|
|
223
|
+
|
|
224
|
+
if obj is None:
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
content = obj.get_content()
|
|
228
|
+
if content is None:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
# Navigate to the target
|
|
232
|
+
if not property_path:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
target = content
|
|
236
|
+
for part in property_path[:-1]:
|
|
237
|
+
if isinstance(target, dict):
|
|
238
|
+
if part in target:
|
|
239
|
+
target = target[part]
|
|
240
|
+
elif create:
|
|
241
|
+
# Create intermediate dict
|
|
242
|
+
target[part] = {}
|
|
243
|
+
target = target[part]
|
|
244
|
+
else:
|
|
245
|
+
return False
|
|
246
|
+
elif isinstance(target, list) and part.isdigit():
|
|
247
|
+
idx = int(part)
|
|
248
|
+
if 0 <= idx < len(target):
|
|
249
|
+
target = target[idx]
|
|
250
|
+
else:
|
|
251
|
+
return False
|
|
252
|
+
else:
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
# Set the value
|
|
256
|
+
final_key = property_path[-1]
|
|
257
|
+
|
|
258
|
+
if isinstance(target, dict):
|
|
259
|
+
# Check if key exists
|
|
260
|
+
key_exists = final_key in target
|
|
261
|
+
|
|
262
|
+
# Convert JSON-style keys to Unity-style if needed
|
|
263
|
+
if not key_exists:
|
|
264
|
+
# Try m_FieldName format
|
|
265
|
+
unity_key = f"m_{final_key[0].upper()}{final_key[1:]}"
|
|
266
|
+
if unity_key in target:
|
|
267
|
+
final_key = unity_key
|
|
268
|
+
key_exists = True
|
|
269
|
+
|
|
270
|
+
if key_exists:
|
|
271
|
+
# Update existing value
|
|
272
|
+
target[final_key] = _convert_value(value)
|
|
273
|
+
return True
|
|
274
|
+
elif create:
|
|
275
|
+
# Create new field (appended at end)
|
|
276
|
+
target[final_key] = _convert_value(value)
|
|
277
|
+
return True
|
|
278
|
+
else:
|
|
279
|
+
return False
|
|
280
|
+
elif isinstance(target, list) and final_key.isdigit():
|
|
281
|
+
idx = int(final_key)
|
|
282
|
+
if 0 <= idx < len(target):
|
|
283
|
+
target[idx] = _convert_value(value)
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def merge_values(
|
|
290
|
+
doc: UnityYAMLDocument,
|
|
291
|
+
path: str,
|
|
292
|
+
values: dict[str, Any],
|
|
293
|
+
*,
|
|
294
|
+
create: bool = True,
|
|
295
|
+
) -> tuple[int, int]:
|
|
296
|
+
"""Merge multiple values into a target path in the document.
|
|
297
|
+
|
|
298
|
+
This is useful for batch inserting multiple fields at once.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
doc: The Unity YAML document to modify
|
|
302
|
+
path: The path to the target dict (e.g., "components/12345")
|
|
303
|
+
values: Dictionary of key-value pairs to merge
|
|
304
|
+
create: If True, create keys that don't exist
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Tuple of (updated_count, created_count)
|
|
308
|
+
|
|
309
|
+
Example:
|
|
310
|
+
merge_values(doc, "components/12345", {
|
|
311
|
+
"portalAPrefab": {"fileID": 123, "guid": "abc", "type": 3},
|
|
312
|
+
"portalBPrefab": {"fileID": 456, "guid": "def", "type": 3},
|
|
313
|
+
"rotationStep": 15
|
|
314
|
+
})
|
|
315
|
+
"""
|
|
316
|
+
parts = path.split("/")
|
|
317
|
+
|
|
318
|
+
if len(parts) < 2:
|
|
319
|
+
return (0, 0)
|
|
320
|
+
|
|
321
|
+
file_id_str = parts[1]
|
|
322
|
+
property_path = parts[2:] if len(parts) > 2 else []
|
|
323
|
+
|
|
324
|
+
# Find the object
|
|
325
|
+
if not file_id_str.isdigit():
|
|
326
|
+
return (0, 0)
|
|
327
|
+
|
|
328
|
+
file_id = int(file_id_str)
|
|
329
|
+
obj = doc.get_by_file_id(file_id)
|
|
330
|
+
|
|
331
|
+
if obj is None:
|
|
332
|
+
return (0, 0)
|
|
333
|
+
|
|
334
|
+
content = obj.get_content()
|
|
335
|
+
if content is None:
|
|
336
|
+
return (0, 0)
|
|
337
|
+
|
|
338
|
+
# Navigate to the target
|
|
339
|
+
target = content
|
|
340
|
+
for part in property_path:
|
|
341
|
+
if isinstance(target, dict):
|
|
342
|
+
if part in target:
|
|
343
|
+
target = target[part]
|
|
344
|
+
elif create:
|
|
345
|
+
target[part] = {}
|
|
346
|
+
target = target[part]
|
|
347
|
+
else:
|
|
348
|
+
return (0, 0)
|
|
349
|
+
elif isinstance(target, list) and part.isdigit():
|
|
350
|
+
idx = int(part)
|
|
351
|
+
if 0 <= idx < len(target):
|
|
352
|
+
target = target[idx]
|
|
353
|
+
else:
|
|
354
|
+
return (0, 0)
|
|
355
|
+
else:
|
|
356
|
+
return (0, 0)
|
|
357
|
+
|
|
358
|
+
if not isinstance(target, dict):
|
|
359
|
+
return (0, 0)
|
|
360
|
+
|
|
361
|
+
updated_count = 0
|
|
362
|
+
created_count = 0
|
|
363
|
+
|
|
364
|
+
for key, value in values.items():
|
|
365
|
+
converted_value = _convert_value(value)
|
|
366
|
+
if key in target:
|
|
367
|
+
target[key] = converted_value
|
|
368
|
+
updated_count += 1
|
|
369
|
+
elif create:
|
|
370
|
+
target[key] = converted_value
|
|
371
|
+
created_count += 1
|
|
372
|
+
|
|
373
|
+
return (updated_count, created_count)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _convert_value(value: Any) -> Any:
|
|
377
|
+
"""Convert a value from JSON format to Unity YAML format."""
|
|
378
|
+
if isinstance(value, dict):
|
|
379
|
+
# Check if it's a vector
|
|
380
|
+
if set(value.keys()) <= {"x", "y", "z", "w"}:
|
|
381
|
+
return {k: float(v) for k, v in value.items()}
|
|
382
|
+
# Check if it's a reference
|
|
383
|
+
if "fileID" in value:
|
|
384
|
+
return value
|
|
385
|
+
# Recursive conversion
|
|
386
|
+
return {k: _convert_value(v) for k, v in value.items()}
|
|
387
|
+
elif isinstance(value, list):
|
|
388
|
+
return [_convert_value(item) for item in value]
|
|
389
|
+
else:
|
|
390
|
+
return value
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def get_value(doc: UnityYAMLDocument, path: str) -> Any | None:
|
|
394
|
+
"""Get a value at a specific path.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
doc: The Unity YAML document
|
|
398
|
+
path: The path to the value
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
The value at the path, or None if not found
|
|
402
|
+
"""
|
|
403
|
+
results = query_path(doc, path)
|
|
404
|
+
if results:
|
|
405
|
+
return results[0].value
|
|
406
|
+
return None
|