@mflrevan/ucp 0.5.0 → 0.5.2
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.
- package/README.md +1 -1
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +7 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +406 -35
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetImportSupport.cs +41 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +88 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/ImporterController.cs +4 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +425 -85
- package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +13 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +9 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs +151 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +303 -8
- package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +80 -4
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +281 -0
- package/bridge/com.ucp.bridge/package.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mflrevan/ucp
|
|
2
2
|
|
|
3
|
-
Version `0.5.
|
|
3
|
+
Version `0.5.2` of the Unity Control Protocol CLI.
|
|
4
4
|
|
|
5
5
|
This package installs the `ucp` command, downloads the matching published binary for your platform during `postinstall`, and ships the matching Unity bridge payload inside the npm package.
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ namespace UCP.Bridge
|
|
|
26
26
|
private const int DefaultPort = 21342;
|
|
27
27
|
private const int MaxPort = 21352;
|
|
28
28
|
private const int MaxConnections = 4;
|
|
29
|
-
private const string ProtocolVersion = "0.5.
|
|
29
|
+
private const string ProtocolVersion = "0.5.2";
|
|
30
30
|
|
|
31
31
|
private static TcpListener s_listener;
|
|
32
32
|
private static CancellationTokenSource s_cts;
|
|
@@ -72,11 +72,12 @@ namespace UCP.Bridge
|
|
|
72
72
|
try
|
|
73
73
|
{
|
|
74
74
|
RegisterHandlers();
|
|
75
|
+
LogsController.SeedHistoryFromConsole();
|
|
75
76
|
|
|
76
77
|
EditorApplication.update += PumpMainThread;
|
|
77
78
|
EditorApplication.quitting += Shutdown;
|
|
78
79
|
AssemblyReloadEvents.beforeAssemblyReload += Shutdown;
|
|
79
|
-
Application.
|
|
80
|
+
Application.logMessageReceivedThreaded += OnLogMessage;
|
|
80
81
|
|
|
81
82
|
s_token = Guid.NewGuid().ToString("N").Substring(0, 16);
|
|
82
83
|
StartServer();
|
|
@@ -165,6 +166,9 @@ namespace UCP.Bridge
|
|
|
165
166
|
|
|
166
167
|
// Reference search (bridge fallback)
|
|
167
168
|
ReferenceController.Register(s_router);
|
|
169
|
+
|
|
170
|
+
// Shader diagnostics
|
|
171
|
+
ShaderController.Register(s_router);
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
private static void StartServer()
|
|
@@ -555,7 +559,7 @@ namespace UCP.Bridge
|
|
|
555
559
|
CleanLockFile();
|
|
556
560
|
|
|
557
561
|
EditorApplication.update -= PumpMainThread;
|
|
558
|
-
Application.
|
|
562
|
+
Application.logMessageReceivedThreaded -= OnLogMessage;
|
|
559
563
|
}
|
|
560
564
|
|
|
561
565
|
private static Dictionary<string, object> ResponseToDict(JsonRpcResponse r)
|
|
@@ -2,8 +2,10 @@ using System;
|
|
|
2
2
|
using System.Collections.Generic;
|
|
3
3
|
using System.IO;
|
|
4
4
|
using System.Reflection;
|
|
5
|
+
using System.Text.RegularExpressions;
|
|
5
6
|
using UnityEditor;
|
|
6
7
|
using UnityEngine;
|
|
8
|
+
using UnityEngine.Rendering;
|
|
7
9
|
|
|
8
10
|
namespace UCP.Bridge
|
|
9
11
|
{
|
|
@@ -13,6 +15,7 @@ namespace UCP.Bridge
|
|
|
13
15
|
{
|
|
14
16
|
router.Register("asset/search", HandleSearch);
|
|
15
17
|
router.Register("asset/info", HandleInfo);
|
|
18
|
+
router.Register("asset/inspect", HandleInspect);
|
|
16
19
|
router.Register("asset/read", HandleReadScriptableObject);
|
|
17
20
|
router.Register("asset/write", HandleWriteScriptableObject);
|
|
18
21
|
router.Register("asset/write-batch", HandleWriteScriptableObjectBatch);
|
|
@@ -28,6 +31,7 @@ namespace UCP.Bridge
|
|
|
28
31
|
string typeFilter = null;
|
|
29
32
|
string nameFilter = null;
|
|
30
33
|
string pathFilter = null;
|
|
34
|
+
bool useRegex = false;
|
|
31
35
|
int maxResults = 100;
|
|
32
36
|
|
|
33
37
|
if (p != null)
|
|
@@ -38,13 +42,28 @@ namespace UCP.Bridge
|
|
|
38
42
|
nameFilter = nObj.ToString();
|
|
39
43
|
if (p.TryGetValue("path", out var pObj) && pObj != null)
|
|
40
44
|
pathFilter = pObj.ToString();
|
|
45
|
+
if (p.TryGetValue("regex", out var regexObj) && regexObj != null)
|
|
46
|
+
useRegex = Convert.ToBoolean(regexObj);
|
|
41
47
|
if (p.TryGetValue("maxResults", out var mObj))
|
|
42
48
|
maxResults = Convert.ToInt32(mObj);
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
Regex nameRegex = null;
|
|
52
|
+
if (useRegex && !string.IsNullOrEmpty(nameFilter))
|
|
53
|
+
{
|
|
54
|
+
try
|
|
55
|
+
{
|
|
56
|
+
nameRegex = new Regex(nameFilter, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
|
57
|
+
}
|
|
58
|
+
catch (ArgumentException ex)
|
|
59
|
+
{
|
|
60
|
+
throw new ArgumentException($"Invalid regex for name filter: {ex.Message}");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
// Build search filter
|
|
46
65
|
string filter = "";
|
|
47
|
-
if (!string.IsNullOrEmpty(nameFilter))
|
|
66
|
+
if (!useRegex && !string.IsNullOrEmpty(nameFilter))
|
|
48
67
|
filter += nameFilter + " ";
|
|
49
68
|
if (!string.IsNullOrEmpty(typeFilter))
|
|
50
69
|
filter += $"t:{GetSearchTypeFilter(typeFilter)}";
|
|
@@ -64,7 +83,7 @@ namespace UCP.Bridge
|
|
|
64
83
|
for (int i = 0; i < guids.Length; i++)
|
|
65
84
|
{
|
|
66
85
|
string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]);
|
|
67
|
-
foreach (var asset in GetMatchingAssets(assetPath, typeFilter, nameFilter))
|
|
86
|
+
foreach (var asset in GetMatchingAssets(assetPath, typeFilter, nameFilter, nameRegex))
|
|
68
87
|
{
|
|
69
88
|
totalMatches++;
|
|
70
89
|
if (results.Count >= maxResults)
|
|
@@ -135,6 +154,47 @@ namespace UCP.Bridge
|
|
|
135
154
|
return info;
|
|
136
155
|
}
|
|
137
156
|
|
|
157
|
+
private static object HandleInspect(string paramsJson)
|
|
158
|
+
{
|
|
159
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
160
|
+
if (p == null || !p.TryGetValue("path", out var pathObj))
|
|
161
|
+
throw new ArgumentException("Missing 'path' parameter");
|
|
162
|
+
|
|
163
|
+
string assetPath = pathObj.ToString();
|
|
164
|
+
int maxFields = 80;
|
|
165
|
+
if (p.TryGetValue("maxFields", out var maxObj) && maxObj != null)
|
|
166
|
+
maxFields = Math.Max(1, Convert.ToInt32(maxObj));
|
|
167
|
+
|
|
168
|
+
var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
|
|
169
|
+
if (asset == null)
|
|
170
|
+
throw new ArgumentException($"Asset not found: {assetPath}");
|
|
171
|
+
|
|
172
|
+
var result = HandleInfo(paramsJson) as Dictionary<string, object>;
|
|
173
|
+
result["inspectedAtUtc"] = DateTime.UtcNow.ToString("o");
|
|
174
|
+
|
|
175
|
+
var importer = AssetImporter.GetAtPath(assetPath);
|
|
176
|
+
if (importer != null)
|
|
177
|
+
result["importer"] = InspectSerializedObject(importer, maxFields);
|
|
178
|
+
|
|
179
|
+
if (asset is Material material)
|
|
180
|
+
{
|
|
181
|
+
result["shader"] = material.shader != null ? material.shader.name : string.Empty;
|
|
182
|
+
result["shaderPath"] = material.shader != null ? AssetDatabase.GetAssetPath(material.shader) : string.Empty;
|
|
183
|
+
result["keywords"] = InspectMaterialKeywords(material);
|
|
184
|
+
result["properties"] = InspectMaterialProperties(material, maxFields);
|
|
185
|
+
}
|
|
186
|
+
else if (asset is GameObject gameObject)
|
|
187
|
+
{
|
|
188
|
+
result["renderers"] = InspectPrefabRenderers(gameObject);
|
|
189
|
+
}
|
|
190
|
+
else
|
|
191
|
+
{
|
|
192
|
+
result["fields"] = InspectSerializedObject(asset, maxFields);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
138
198
|
private static object HandleReadScriptableObject(string paramsJson)
|
|
139
199
|
{
|
|
140
200
|
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
@@ -194,6 +254,133 @@ namespace UCP.Bridge
|
|
|
194
254
|
};
|
|
195
255
|
}
|
|
196
256
|
|
|
257
|
+
private static List<object> InspectSerializedObject(UnityEngine.Object target, int maxFields)
|
|
258
|
+
{
|
|
259
|
+
var serializedObject = new SerializedObject(target);
|
|
260
|
+
var fields = new List<object>();
|
|
261
|
+
try
|
|
262
|
+
{
|
|
263
|
+
var iterator = serializedObject.GetIterator();
|
|
264
|
+
if (iterator.NextVisible(true))
|
|
265
|
+
{
|
|
266
|
+
do
|
|
267
|
+
{
|
|
268
|
+
if (iterator.name == "m_Script") continue;
|
|
269
|
+
fields.Add(SerializedPropertyControllerSupport.Describe(iterator));
|
|
270
|
+
}
|
|
271
|
+
while (fields.Count < maxFields && iterator.NextVisible(false));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
finally
|
|
275
|
+
{
|
|
276
|
+
serializedObject.Dispose();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return fields;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private static List<object> InspectMaterialKeywords(Material material)
|
|
283
|
+
{
|
|
284
|
+
var keywords = new List<object>();
|
|
285
|
+
foreach (var keyword in material.enabledKeywords)
|
|
286
|
+
keywords.Add(keyword.name);
|
|
287
|
+
return keywords;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private static List<object> InspectMaterialProperties(Material material, int maxFields)
|
|
291
|
+
{
|
|
292
|
+
var properties = new List<object>();
|
|
293
|
+
var shader = material.shader;
|
|
294
|
+
if (shader == null)
|
|
295
|
+
return properties;
|
|
296
|
+
|
|
297
|
+
var count = Math.Min(shader.GetPropertyCount(), maxFields);
|
|
298
|
+
for (int i = 0; i < count; i++)
|
|
299
|
+
{
|
|
300
|
+
var name = shader.GetPropertyName(i);
|
|
301
|
+
var type = shader.GetPropertyType(i);
|
|
302
|
+
properties.Add(new Dictionary<string, object>
|
|
303
|
+
{
|
|
304
|
+
["name"] = name,
|
|
305
|
+
["description"] = shader.GetPropertyDescription(i),
|
|
306
|
+
["type"] = type.ToString(),
|
|
307
|
+
["value"] = ReadMaterialValue(material, name, type)
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return properties;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private static object ReadMaterialValue(Material material, string name, ShaderPropertyType type)
|
|
315
|
+
{
|
|
316
|
+
switch (type)
|
|
317
|
+
{
|
|
318
|
+
case ShaderPropertyType.Color:
|
|
319
|
+
var color = material.GetColor(name);
|
|
320
|
+
return new List<object> { color.r, color.g, color.b, color.a };
|
|
321
|
+
case ShaderPropertyType.Vector:
|
|
322
|
+
var vector = material.GetVector(name);
|
|
323
|
+
return new List<object> { vector.x, vector.y, vector.z, vector.w };
|
|
324
|
+
case ShaderPropertyType.Float:
|
|
325
|
+
case ShaderPropertyType.Range:
|
|
326
|
+
return material.GetFloat(name);
|
|
327
|
+
case ShaderPropertyType.Texture:
|
|
328
|
+
var texture = material.GetTexture(name);
|
|
329
|
+
return texture != null
|
|
330
|
+
? new Dictionary<string, object>
|
|
331
|
+
{
|
|
332
|
+
["name"] = texture.name,
|
|
333
|
+
["path"] = AssetDatabase.GetAssetPath(texture),
|
|
334
|
+
["type"] = texture.GetType().Name
|
|
335
|
+
}
|
|
336
|
+
: null;
|
|
337
|
+
default:
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private static List<object> InspectPrefabRenderers(GameObject gameObject)
|
|
343
|
+
{
|
|
344
|
+
var renderers = new List<object>();
|
|
345
|
+
foreach (var renderer in gameObject.GetComponentsInChildren<Renderer>(true))
|
|
346
|
+
{
|
|
347
|
+
var materials = new List<object>();
|
|
348
|
+
foreach (var material in renderer.sharedMaterials)
|
|
349
|
+
{
|
|
350
|
+
materials.Add(material != null
|
|
351
|
+
? new Dictionary<string, object>
|
|
352
|
+
{
|
|
353
|
+
["name"] = material.name,
|
|
354
|
+
["path"] = AssetDatabase.GetAssetPath(material),
|
|
355
|
+
["shader"] = material.shader != null ? material.shader.name : string.Empty
|
|
356
|
+
}
|
|
357
|
+
: null);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
renderers.Add(new Dictionary<string, object>
|
|
361
|
+
{
|
|
362
|
+
["path"] = GetTransformPath(renderer.transform),
|
|
363
|
+
["type"] = renderer.GetType().Name,
|
|
364
|
+
["enabled"] = renderer.enabled,
|
|
365
|
+
["materials"] = materials
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return renderers;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private static string GetTransformPath(Transform transform)
|
|
373
|
+
{
|
|
374
|
+
var names = new List<string>();
|
|
375
|
+
var current = transform;
|
|
376
|
+
while (current != null)
|
|
377
|
+
{
|
|
378
|
+
names.Insert(0, current.name);
|
|
379
|
+
current = current.parent;
|
|
380
|
+
}
|
|
381
|
+
return string.Join("/", names);
|
|
382
|
+
}
|
|
383
|
+
|
|
197
384
|
private static object HandleWriteScriptableObject(string paramsJson)
|
|
198
385
|
{
|
|
199
386
|
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
@@ -354,6 +541,7 @@ namespace UCP.Bridge
|
|
|
354
541
|
throw new ArgumentException("Missing 'moves' parameter");
|
|
355
542
|
|
|
356
543
|
var continueOnError = TryGetOptionalBool(parameters, "continueOnError");
|
|
544
|
+
var dryRun = TryGetOptionalBool(parameters, "dryRun");
|
|
357
545
|
var requests = ParseMoveRequests(movesObject);
|
|
358
546
|
var results = new List<object>();
|
|
359
547
|
var errors = new List<object>();
|
|
@@ -361,15 +549,14 @@ namespace UCP.Bridge
|
|
|
361
549
|
var stopped = false;
|
|
362
550
|
var anyChanged = false;
|
|
363
551
|
|
|
364
|
-
|
|
365
|
-
try
|
|
552
|
+
if (dryRun)
|
|
366
553
|
{
|
|
367
554
|
for (var index = 0; index < requests.Count; index++)
|
|
368
555
|
{
|
|
369
556
|
var request = requests[index];
|
|
370
557
|
try
|
|
371
558
|
{
|
|
372
|
-
var moveResult =
|
|
559
|
+
var moveResult = PreviewMoveInternal(request.SourcePath, request.Destination);
|
|
373
560
|
results.Add(AddMoveIndex(moveResult, index));
|
|
374
561
|
if (GetBool(moveResult, "changed"))
|
|
375
562
|
{
|
|
@@ -395,12 +582,75 @@ namespace UCP.Bridge
|
|
|
395
582
|
}
|
|
396
583
|
}
|
|
397
584
|
}
|
|
398
|
-
|
|
585
|
+
else
|
|
399
586
|
{
|
|
400
|
-
|
|
587
|
+
var preparedMoves = new List<PreparedMove>();
|
|
588
|
+
for (var index = 0; index < requests.Count; index++)
|
|
589
|
+
{
|
|
590
|
+
var request = requests[index];
|
|
591
|
+
try
|
|
592
|
+
{
|
|
593
|
+
var preparedMove = PrepareMove(request.SourcePath, request.Destination, true);
|
|
594
|
+
preparedMoves.Add(new PreparedMove(index, request.SourcePath, preparedMove));
|
|
595
|
+
}
|
|
596
|
+
catch (Exception ex)
|
|
597
|
+
{
|
|
598
|
+
errors.Add(new Dictionary<string, object>
|
|
599
|
+
{
|
|
600
|
+
["index"] = index,
|
|
601
|
+
["sourcePath"] = request.SourcePath,
|
|
602
|
+
["destinationPath"] = request.Destination,
|
|
603
|
+
["message"] = ex.Message
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
if (!continueOnError)
|
|
607
|
+
{
|
|
608
|
+
stopped = true;
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
AssetDatabase.StartAssetEditing();
|
|
615
|
+
try
|
|
616
|
+
{
|
|
617
|
+
foreach (var preparedMove in preparedMoves)
|
|
618
|
+
{
|
|
619
|
+
try
|
|
620
|
+
{
|
|
621
|
+
var moveResult = MovePreparedAsset(preparedMove.SourcePath, preparedMove.Move, false);
|
|
622
|
+
results.Add(AddMoveIndex(moveResult, preparedMove.Index));
|
|
623
|
+
if (GetBool(moveResult, "changed"))
|
|
624
|
+
{
|
|
625
|
+
movedCount++;
|
|
626
|
+
anyChanged = true;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
catch (Exception ex)
|
|
630
|
+
{
|
|
631
|
+
errors.Add(new Dictionary<string, object>
|
|
632
|
+
{
|
|
633
|
+
["index"] = preparedMove.Index,
|
|
634
|
+
["sourcePath"] = preparedMove.SourcePath,
|
|
635
|
+
["destinationPath"] = preparedMove.Move.ResolvedDestination,
|
|
636
|
+
["message"] = ex.Message
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
if (!continueOnError)
|
|
640
|
+
{
|
|
641
|
+
stopped = true;
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
finally
|
|
648
|
+
{
|
|
649
|
+
AssetDatabase.StopAssetEditing();
|
|
650
|
+
}
|
|
401
651
|
}
|
|
402
652
|
|
|
403
|
-
if (anyChanged)
|
|
653
|
+
if (!dryRun && anyChanged)
|
|
404
654
|
{
|
|
405
655
|
AssetDatabase.SaveAssets();
|
|
406
656
|
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
@@ -412,6 +662,7 @@ namespace UCP.Bridge
|
|
|
412
662
|
["requested"] = requests.Count,
|
|
413
663
|
["moved"] = movedCount,
|
|
414
664
|
["failed"] = errors.Count,
|
|
665
|
+
["dryRun"] = dryRun,
|
|
415
666
|
["stopped"] = stopped,
|
|
416
667
|
["results"] = results,
|
|
417
668
|
["errors"] = errors
|
|
@@ -431,10 +682,19 @@ namespace UCP.Bridge
|
|
|
431
682
|
}
|
|
432
683
|
}
|
|
433
684
|
|
|
434
|
-
private static IEnumerable<UnityEngine.Object> GetMatchingAssets(
|
|
685
|
+
private static IEnumerable<UnityEngine.Object> GetMatchingAssets(
|
|
686
|
+
string assetPath,
|
|
687
|
+
string typeFilter,
|
|
688
|
+
string nameFilter,
|
|
689
|
+
Regex nameRegex)
|
|
435
690
|
{
|
|
436
691
|
UnityEngine.Object[] candidates;
|
|
437
|
-
if (string.IsNullOrEmpty(typeFilter) && string.IsNullOrEmpty(nameFilter))
|
|
692
|
+
if (string.IsNullOrEmpty(typeFilter) && string.IsNullOrEmpty(nameFilter) && nameRegex == null)
|
|
693
|
+
{
|
|
694
|
+
var mainAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
|
695
|
+
candidates = mainAsset != null ? new[] { mainAsset } : Array.Empty<UnityEngine.Object>();
|
|
696
|
+
}
|
|
697
|
+
else if (assetPath.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
|
|
438
698
|
{
|
|
439
699
|
var mainAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
|
440
700
|
candidates = mainAsset != null ? new[] { mainAsset } : Array.Empty<UnityEngine.Object>();
|
|
@@ -447,17 +707,24 @@ namespace UCP.Bridge
|
|
|
447
707
|
foreach (var candidate in candidates)
|
|
448
708
|
{
|
|
449
709
|
if (candidate == null) continue;
|
|
450
|
-
if (!MatchesName(candidate, assetPath, nameFilter)) continue;
|
|
710
|
+
if (!MatchesName(candidate, assetPath, nameFilter, nameRegex)) continue;
|
|
451
711
|
if (!MatchesType(candidate, assetPath, typeFilter)) continue;
|
|
452
712
|
yield return candidate;
|
|
453
713
|
}
|
|
454
714
|
}
|
|
455
715
|
|
|
456
|
-
private static bool MatchesName(UnityEngine.Object asset, string assetPath, string nameFilter)
|
|
716
|
+
private static bool MatchesName(UnityEngine.Object asset, string assetPath, string nameFilter, Regex nameRegex)
|
|
457
717
|
{
|
|
458
|
-
if (string.IsNullOrEmpty(nameFilter))
|
|
718
|
+
if (string.IsNullOrEmpty(nameFilter) && nameRegex == null)
|
|
459
719
|
return true;
|
|
460
720
|
|
|
721
|
+
if (nameRegex != null)
|
|
722
|
+
{
|
|
723
|
+
var fileStem = System.IO.Path.GetFileNameWithoutExtension(assetPath);
|
|
724
|
+
return nameRegex.IsMatch(asset.name)
|
|
725
|
+
|| (!string.IsNullOrEmpty(fileStem) && nameRegex.IsMatch(fileStem));
|
|
726
|
+
}
|
|
727
|
+
|
|
461
728
|
return asset.name.Contains(nameFilter, StringComparison.OrdinalIgnoreCase)
|
|
462
729
|
|| System.IO.Path.GetFileNameWithoutExtension(assetPath).Contains(nameFilter, StringComparison.OrdinalIgnoreCase);
|
|
463
730
|
}
|
|
@@ -507,30 +774,29 @@ namespace UCP.Bridge
|
|
|
507
774
|
|
|
508
775
|
private static Dictionary<string, object> MoveAssetInternal(string sourcePath, string destination, bool finalize)
|
|
509
776
|
{
|
|
510
|
-
var
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
throw new ArgumentException($"Asset not found: {normalizedSource}");
|
|
514
|
-
|
|
515
|
-
if (!IsMovableAssetPath(normalizedSource))
|
|
516
|
-
throw new ArgumentException($"Asset moves are only supported under Assets/: {normalizedSource}");
|
|
517
|
-
|
|
518
|
-
var identity = DescribeExistingAsset(normalizedSource, isFolder);
|
|
519
|
-
var resolvedDestination = ResolveDestinationPath(normalizedSource, destination);
|
|
520
|
-
if (!IsMovableAssetPath(resolvedDestination))
|
|
521
|
-
throw new ArgumentException($"Destination must be under Assets/: {resolvedDestination}");
|
|
522
|
-
|
|
523
|
-
if (string.Equals(normalizedSource, resolvedDestination, StringComparison.OrdinalIgnoreCase))
|
|
524
|
-
{
|
|
525
|
-
return DescribeMovedAsset(normalizedSource, resolvedDestination, false, identity);
|
|
526
|
-
}
|
|
777
|
+
var move = PrepareMove(sourcePath, destination, true);
|
|
778
|
+
return MovePreparedAsset(NormalizeMovePath(sourcePath), move, finalize);
|
|
779
|
+
}
|
|
527
780
|
|
|
528
|
-
|
|
529
|
-
|
|
781
|
+
private static Dictionary<string, object> PreviewMoveInternal(string sourcePath, string destination)
|
|
782
|
+
{
|
|
783
|
+
var normalizedSource = NormalizeMovePath(sourcePath);
|
|
784
|
+
var move = PrepareMove(normalizedSource, destination, false);
|
|
785
|
+
return DescribeMovedAsset(normalizedSource, move.ResolvedDestination, move.Changed, move.Identity);
|
|
786
|
+
}
|
|
530
787
|
|
|
531
|
-
|
|
788
|
+
private static ValidatedMove PrepareMove(string sourcePath, string destination, bool ensureParentFolders)
|
|
789
|
+
{
|
|
790
|
+
var normalizedSource = NormalizeMovePath(sourcePath);
|
|
791
|
+
var move = ValidateMoveRequest(normalizedSource, destination);
|
|
792
|
+
if (ensureParentFolders)
|
|
793
|
+
EnsureParentFoldersExist(move.ResolvedDestination);
|
|
794
|
+
return move;
|
|
795
|
+
}
|
|
532
796
|
|
|
533
|
-
|
|
797
|
+
private static Dictionary<string, object> MovePreparedAsset(string normalizedSource, ValidatedMove move, bool finalize)
|
|
798
|
+
{
|
|
799
|
+
var moveError = AssetDatabase.MoveAsset(normalizedSource, move.ResolvedDestination);
|
|
534
800
|
if (!string.IsNullOrEmpty(moveError))
|
|
535
801
|
throw new InvalidOperationException(moveError);
|
|
536
802
|
|
|
@@ -540,7 +806,7 @@ namespace UCP.Bridge
|
|
|
540
806
|
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
541
807
|
}
|
|
542
808
|
|
|
543
|
-
return DescribeMovedAsset(normalizedSource,
|
|
809
|
+
return DescribeMovedAsset(normalizedSource, move.ResolvedDestination, true, move.Identity);
|
|
544
810
|
}
|
|
545
811
|
|
|
546
812
|
private static Dictionary<string, object> DescribeMovedAsset(
|
|
@@ -648,6 +914,29 @@ namespace UCP.Bridge
|
|
|
648
914
|
return path?.Trim().Replace('\\', '/');
|
|
649
915
|
}
|
|
650
916
|
|
|
917
|
+
private static ValidatedMove ValidateMoveRequest(string normalizedSource, string destination)
|
|
918
|
+
{
|
|
919
|
+
var isFolder = AssetDatabase.IsValidFolder(normalizedSource);
|
|
920
|
+
if (!isFolder && !AssetDatabase.AssetPathExists(normalizedSource))
|
|
921
|
+
throw new ArgumentException(BuildAssetNotFoundMessage(normalizedSource));
|
|
922
|
+
|
|
923
|
+
if (!IsMovableAssetPath(normalizedSource))
|
|
924
|
+
throw new ArgumentException($"Asset moves are only supported under Assets/: {normalizedSource}");
|
|
925
|
+
|
|
926
|
+
var identity = DescribeExistingAsset(normalizedSource, isFolder);
|
|
927
|
+
var resolvedDestination = ResolveDestinationPath(normalizedSource, destination);
|
|
928
|
+
if (!IsMovableAssetPath(resolvedDestination))
|
|
929
|
+
throw new ArgumentException($"Destination must be under Assets/: {resolvedDestination}");
|
|
930
|
+
|
|
931
|
+
if (string.Equals(normalizedSource, resolvedDestination, StringComparison.OrdinalIgnoreCase))
|
|
932
|
+
return new ValidatedMove(identity, resolvedDestination, false);
|
|
933
|
+
|
|
934
|
+
if (AssetDatabase.AssetPathExists(resolvedDestination) || AssetDatabase.IsValidFolder(resolvedDestination))
|
|
935
|
+
throw new ArgumentException($"Destination already exists: {resolvedDestination}");
|
|
936
|
+
|
|
937
|
+
return new ValidatedMove(identity, resolvedDestination, true);
|
|
938
|
+
}
|
|
939
|
+
|
|
651
940
|
private static bool IsMovableAssetPath(string path)
|
|
652
941
|
{
|
|
653
942
|
return !string.IsNullOrEmpty(path)
|
|
@@ -655,6 +944,60 @@ namespace UCP.Bridge
|
|
|
655
944
|
|| path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase));
|
|
656
945
|
}
|
|
657
946
|
|
|
947
|
+
private static string BuildAssetNotFoundMessage(string normalizedSource)
|
|
948
|
+
{
|
|
949
|
+
var message = $"Asset not found: {normalizedSource}.";
|
|
950
|
+
var suggestions = FindSimilarAssetPaths(normalizedSource, 3);
|
|
951
|
+
if (suggestions.Count > 0)
|
|
952
|
+
message += " Did you mean: " + string.Join(", ", suggestions) + "?";
|
|
953
|
+
message += " If the asset was created or renamed outside Unity, refresh or reimport so AssetDatabase can pick up the latest path.";
|
|
954
|
+
return message;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
private static List<string> FindSimilarAssetPaths(string normalizedSource, int maxSuggestions)
|
|
958
|
+
{
|
|
959
|
+
var targetName = Path.GetFileNameWithoutExtension(normalizedSource) ?? normalizedSource;
|
|
960
|
+
var targetFileName = Path.GetFileName(normalizedSource) ?? normalizedSource;
|
|
961
|
+
var targetExtension = Path.GetExtension(normalizedSource) ?? string.Empty;
|
|
962
|
+
var matches = new List<(string path, int score)>();
|
|
963
|
+
|
|
964
|
+
foreach (var candidate in AssetDatabase.GetAllAssetPaths())
|
|
965
|
+
{
|
|
966
|
+
if (!candidate.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
|
967
|
+
continue;
|
|
968
|
+
|
|
969
|
+
var score = 0;
|
|
970
|
+
if (candidate.IndexOf(targetName, StringComparison.OrdinalIgnoreCase) >= 0)
|
|
971
|
+
score += 3;
|
|
972
|
+
if (string.Equals(Path.GetFileName(candidate), targetFileName, StringComparison.OrdinalIgnoreCase))
|
|
973
|
+
score += 4;
|
|
974
|
+
if (string.Equals(Path.GetExtension(candidate), targetExtension, StringComparison.OrdinalIgnoreCase))
|
|
975
|
+
score += 1;
|
|
976
|
+
if (score > 0)
|
|
977
|
+
matches.Add((candidate, score));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
matches.Sort((left, right) =>
|
|
981
|
+
{
|
|
982
|
+
var scoreCompare = right.score.CompareTo(left.score);
|
|
983
|
+
return scoreCompare != 0
|
|
984
|
+
? scoreCompare
|
|
985
|
+
: string.Compare(left.path, right.path, StringComparison.OrdinalIgnoreCase);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
var suggestions = new List<string>();
|
|
989
|
+
foreach (var match in matches)
|
|
990
|
+
{
|
|
991
|
+
if (suggestions.Contains(match.path))
|
|
992
|
+
continue;
|
|
993
|
+
suggestions.Add(match.path);
|
|
994
|
+
if (suggestions.Count >= maxSuggestions)
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return suggestions;
|
|
999
|
+
}
|
|
1000
|
+
|
|
658
1001
|
private static Dictionary<string, object> ParseParameters(string paramsJson)
|
|
659
1002
|
{
|
|
660
1003
|
return MiniJson.Deserialize(paramsJson) as Dictionary<string, object>
|
|
@@ -750,6 +1093,20 @@ namespace UCP.Bridge
|
|
|
750
1093
|
public string Destination { get; }
|
|
751
1094
|
}
|
|
752
1095
|
|
|
1096
|
+
private readonly struct PreparedMove
|
|
1097
|
+
{
|
|
1098
|
+
public PreparedMove(int index, string sourcePath, ValidatedMove move)
|
|
1099
|
+
{
|
|
1100
|
+
Index = index;
|
|
1101
|
+
SourcePath = sourcePath;
|
|
1102
|
+
Move = move;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
public int Index { get; }
|
|
1106
|
+
public string SourcePath { get; }
|
|
1107
|
+
public ValidatedMove Move { get; }
|
|
1108
|
+
}
|
|
1109
|
+
|
|
753
1110
|
private readonly struct AssetIdentity
|
|
754
1111
|
{
|
|
755
1112
|
public AssetIdentity(string guid, string name, string type, bool isFolder)
|
|
@@ -766,5 +1123,19 @@ namespace UCP.Bridge
|
|
|
766
1123
|
public bool IsFolder { get; }
|
|
767
1124
|
}
|
|
768
1125
|
|
|
1126
|
+
private readonly struct ValidatedMove
|
|
1127
|
+
{
|
|
1128
|
+
public ValidatedMove(AssetIdentity identity, string resolvedDestination, bool changed)
|
|
1129
|
+
{
|
|
1130
|
+
Identity = identity;
|
|
1131
|
+
ResolvedDestination = resolvedDestination;
|
|
1132
|
+
Changed = changed;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
public AssetIdentity Identity { get; }
|
|
1136
|
+
public string ResolvedDestination { get; }
|
|
1137
|
+
public bool Changed { get; }
|
|
1138
|
+
}
|
|
1139
|
+
|
|
769
1140
|
}
|
|
770
1141
|
}
|