@mflrevan/ucp 0.4.6 → 0.5.1
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 +4 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +553 -7
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetImportSupport.cs +41 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ImporterController.cs +4 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +103 -75
- package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs +163 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +9 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +80 -4
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +375 -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.
|
|
3
|
+
Version `0.5.1` 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.
|
|
29
|
+
private const string ProtocolVersion = "0.5.1";
|
|
30
30
|
|
|
31
31
|
private static TcpListener s_listener;
|
|
32
32
|
private static CancellationTokenSource s_cts;
|
|
@@ -162,6 +162,9 @@ namespace UCP.Bridge
|
|
|
162
162
|
|
|
163
163
|
// Package management
|
|
164
164
|
PackagesController.Register(s_router);
|
|
165
|
+
|
|
166
|
+
// Reference search (bridge fallback)
|
|
167
|
+
ReferenceController.Register(s_router);
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
private static void StartServer()
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
using System;
|
|
2
2
|
using System.Collections.Generic;
|
|
3
|
+
using System.IO;
|
|
3
4
|
using System.Reflection;
|
|
5
|
+
using System.Text.RegularExpressions;
|
|
4
6
|
using UnityEditor;
|
|
5
7
|
using UnityEngine;
|
|
6
8
|
|
|
@@ -17,6 +19,8 @@ namespace UCP.Bridge
|
|
|
17
19
|
router.Register("asset/write-batch", HandleWriteScriptableObjectBatch);
|
|
18
20
|
router.Register("asset/create-so", HandleCreateScriptableObject);
|
|
19
21
|
router.Register("asset/delete", HandleDeleteAsset);
|
|
22
|
+
router.Register("asset/move", HandleMoveAsset);
|
|
23
|
+
router.Register("asset/bulk-move", HandleBulkMoveAssets);
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
private static object HandleSearch(string paramsJson)
|
|
@@ -25,6 +29,7 @@ namespace UCP.Bridge
|
|
|
25
29
|
string typeFilter = null;
|
|
26
30
|
string nameFilter = null;
|
|
27
31
|
string pathFilter = null;
|
|
32
|
+
bool useRegex = false;
|
|
28
33
|
int maxResults = 100;
|
|
29
34
|
|
|
30
35
|
if (p != null)
|
|
@@ -35,13 +40,28 @@ namespace UCP.Bridge
|
|
|
35
40
|
nameFilter = nObj.ToString();
|
|
36
41
|
if (p.TryGetValue("path", out var pObj) && pObj != null)
|
|
37
42
|
pathFilter = pObj.ToString();
|
|
43
|
+
if (p.TryGetValue("regex", out var regexObj) && regexObj != null)
|
|
44
|
+
useRegex = Convert.ToBoolean(regexObj);
|
|
38
45
|
if (p.TryGetValue("maxResults", out var mObj))
|
|
39
46
|
maxResults = Convert.ToInt32(mObj);
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
Regex nameRegex = null;
|
|
50
|
+
if (useRegex && !string.IsNullOrEmpty(nameFilter))
|
|
51
|
+
{
|
|
52
|
+
try
|
|
53
|
+
{
|
|
54
|
+
nameRegex = new Regex(nameFilter, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
|
55
|
+
}
|
|
56
|
+
catch (ArgumentException ex)
|
|
57
|
+
{
|
|
58
|
+
throw new ArgumentException($"Invalid regex for name filter: {ex.Message}");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
// Build search filter
|
|
43
63
|
string filter = "";
|
|
44
|
-
if (!string.IsNullOrEmpty(nameFilter))
|
|
64
|
+
if (!useRegex && !string.IsNullOrEmpty(nameFilter))
|
|
45
65
|
filter += nameFilter + " ";
|
|
46
66
|
if (!string.IsNullOrEmpty(typeFilter))
|
|
47
67
|
filter += $"t:{GetSearchTypeFilter(typeFilter)}";
|
|
@@ -61,7 +81,7 @@ namespace UCP.Bridge
|
|
|
61
81
|
for (int i = 0; i < guids.Length; i++)
|
|
62
82
|
{
|
|
63
83
|
string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]);
|
|
64
|
-
foreach (var asset in GetMatchingAssets(assetPath, typeFilter, nameFilter))
|
|
84
|
+
foreach (var asset in GetMatchingAssets(assetPath, typeFilter, nameFilter, nameRegex))
|
|
65
85
|
{
|
|
66
86
|
totalMatches++;
|
|
67
87
|
if (results.Count >= maxResults)
|
|
@@ -334,6 +354,151 @@ namespace UCP.Bridge
|
|
|
334
354
|
};
|
|
335
355
|
}
|
|
336
356
|
|
|
357
|
+
private static object HandleMoveAsset(string paramsJson)
|
|
358
|
+
{
|
|
359
|
+
var parameters = ParseParameters(paramsJson);
|
|
360
|
+
var sourcePath = RequirePathParameter(parameters, "path");
|
|
361
|
+
var destination = RequirePathParameter(parameters, "destination");
|
|
362
|
+
|
|
363
|
+
var result = MoveAssetInternal(sourcePath, destination, true);
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private static object HandleBulkMoveAssets(string paramsJson)
|
|
368
|
+
{
|
|
369
|
+
var parameters = ParseParameters(paramsJson);
|
|
370
|
+
if (!parameters.TryGetValue("moves", out var movesObject) || movesObject == null)
|
|
371
|
+
throw new ArgumentException("Missing 'moves' parameter");
|
|
372
|
+
|
|
373
|
+
var continueOnError = TryGetOptionalBool(parameters, "continueOnError");
|
|
374
|
+
var dryRun = TryGetOptionalBool(parameters, "dryRun");
|
|
375
|
+
var requests = ParseMoveRequests(movesObject);
|
|
376
|
+
var results = new List<object>();
|
|
377
|
+
var errors = new List<object>();
|
|
378
|
+
var movedCount = 0;
|
|
379
|
+
var stopped = false;
|
|
380
|
+
var anyChanged = false;
|
|
381
|
+
|
|
382
|
+
if (dryRun)
|
|
383
|
+
{
|
|
384
|
+
for (var index = 0; index < requests.Count; index++)
|
|
385
|
+
{
|
|
386
|
+
var request = requests[index];
|
|
387
|
+
try
|
|
388
|
+
{
|
|
389
|
+
var moveResult = PreviewMoveInternal(request.SourcePath, request.Destination);
|
|
390
|
+
results.Add(AddMoveIndex(moveResult, index));
|
|
391
|
+
if (GetBool(moveResult, "changed"))
|
|
392
|
+
{
|
|
393
|
+
movedCount++;
|
|
394
|
+
anyChanged = true;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (Exception ex)
|
|
398
|
+
{
|
|
399
|
+
errors.Add(new Dictionary<string, object>
|
|
400
|
+
{
|
|
401
|
+
["index"] = index,
|
|
402
|
+
["sourcePath"] = request.SourcePath,
|
|
403
|
+
["destinationPath"] = request.Destination,
|
|
404
|
+
["message"] = ex.Message
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (!continueOnError)
|
|
408
|
+
{
|
|
409
|
+
stopped = true;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else
|
|
416
|
+
{
|
|
417
|
+
var preparedMoves = new List<PreparedMove>();
|
|
418
|
+
for (var index = 0; index < requests.Count; index++)
|
|
419
|
+
{
|
|
420
|
+
var request = requests[index];
|
|
421
|
+
try
|
|
422
|
+
{
|
|
423
|
+
var preparedMove = PrepareMove(request.SourcePath, request.Destination, true);
|
|
424
|
+
preparedMoves.Add(new PreparedMove(index, request.SourcePath, preparedMove));
|
|
425
|
+
}
|
|
426
|
+
catch (Exception ex)
|
|
427
|
+
{
|
|
428
|
+
errors.Add(new Dictionary<string, object>
|
|
429
|
+
{
|
|
430
|
+
["index"] = index,
|
|
431
|
+
["sourcePath"] = request.SourcePath,
|
|
432
|
+
["destinationPath"] = request.Destination,
|
|
433
|
+
["message"] = ex.Message
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
if (!continueOnError)
|
|
437
|
+
{
|
|
438
|
+
stopped = true;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
AssetDatabase.StartAssetEditing();
|
|
445
|
+
try
|
|
446
|
+
{
|
|
447
|
+
foreach (var preparedMove in preparedMoves)
|
|
448
|
+
{
|
|
449
|
+
try
|
|
450
|
+
{
|
|
451
|
+
var moveResult = MovePreparedAsset(preparedMove.SourcePath, preparedMove.Move, false);
|
|
452
|
+
results.Add(AddMoveIndex(moveResult, preparedMove.Index));
|
|
453
|
+
if (GetBool(moveResult, "changed"))
|
|
454
|
+
{
|
|
455
|
+
movedCount++;
|
|
456
|
+
anyChanged = true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch (Exception ex)
|
|
460
|
+
{
|
|
461
|
+
errors.Add(new Dictionary<string, object>
|
|
462
|
+
{
|
|
463
|
+
["index"] = preparedMove.Index,
|
|
464
|
+
["sourcePath"] = preparedMove.SourcePath,
|
|
465
|
+
["destinationPath"] = preparedMove.Move.ResolvedDestination,
|
|
466
|
+
["message"] = ex.Message
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (!continueOnError)
|
|
470
|
+
{
|
|
471
|
+
stopped = true;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
finally
|
|
478
|
+
{
|
|
479
|
+
AssetDatabase.StopAssetEditing();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!dryRun && anyChanged)
|
|
484
|
+
{
|
|
485
|
+
AssetDatabase.SaveAssets();
|
|
486
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return new Dictionary<string, object>
|
|
490
|
+
{
|
|
491
|
+
["status"] = "ok",
|
|
492
|
+
["requested"] = requests.Count,
|
|
493
|
+
["moved"] = movedCount,
|
|
494
|
+
["failed"] = errors.Count,
|
|
495
|
+
["dryRun"] = dryRun,
|
|
496
|
+
["stopped"] = stopped,
|
|
497
|
+
["results"] = results,
|
|
498
|
+
["errors"] = errors
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
337
502
|
private static void CreateFoldersRecursive(string path)
|
|
338
503
|
{
|
|
339
504
|
var parts = path.Replace("\\", "/").Split('/');
|
|
@@ -347,10 +512,19 @@ namespace UCP.Bridge
|
|
|
347
512
|
}
|
|
348
513
|
}
|
|
349
514
|
|
|
350
|
-
private static IEnumerable<UnityEngine.Object> GetMatchingAssets(
|
|
515
|
+
private static IEnumerable<UnityEngine.Object> GetMatchingAssets(
|
|
516
|
+
string assetPath,
|
|
517
|
+
string typeFilter,
|
|
518
|
+
string nameFilter,
|
|
519
|
+
Regex nameRegex)
|
|
351
520
|
{
|
|
352
521
|
UnityEngine.Object[] candidates;
|
|
353
|
-
if (string.IsNullOrEmpty(typeFilter) && string.IsNullOrEmpty(nameFilter))
|
|
522
|
+
if (string.IsNullOrEmpty(typeFilter) && string.IsNullOrEmpty(nameFilter) && nameRegex == null)
|
|
523
|
+
{
|
|
524
|
+
var mainAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
|
525
|
+
candidates = mainAsset != null ? new[] { mainAsset } : Array.Empty<UnityEngine.Object>();
|
|
526
|
+
}
|
|
527
|
+
else if (assetPath.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
|
|
354
528
|
{
|
|
355
529
|
var mainAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
|
356
530
|
candidates = mainAsset != null ? new[] { mainAsset } : Array.Empty<UnityEngine.Object>();
|
|
@@ -363,17 +537,24 @@ namespace UCP.Bridge
|
|
|
363
537
|
foreach (var candidate in candidates)
|
|
364
538
|
{
|
|
365
539
|
if (candidate == null) continue;
|
|
366
|
-
if (!MatchesName(candidate, assetPath, nameFilter)) continue;
|
|
540
|
+
if (!MatchesName(candidate, assetPath, nameFilter, nameRegex)) continue;
|
|
367
541
|
if (!MatchesType(candidate, assetPath, typeFilter)) continue;
|
|
368
542
|
yield return candidate;
|
|
369
543
|
}
|
|
370
544
|
}
|
|
371
545
|
|
|
372
|
-
private static bool MatchesName(UnityEngine.Object asset, string assetPath, string nameFilter)
|
|
546
|
+
private static bool MatchesName(UnityEngine.Object asset, string assetPath, string nameFilter, Regex nameRegex)
|
|
373
547
|
{
|
|
374
|
-
if (string.IsNullOrEmpty(nameFilter))
|
|
548
|
+
if (string.IsNullOrEmpty(nameFilter) && nameRegex == null)
|
|
375
549
|
return true;
|
|
376
550
|
|
|
551
|
+
if (nameRegex != null)
|
|
552
|
+
{
|
|
553
|
+
var fileStem = System.IO.Path.GetFileNameWithoutExtension(assetPath);
|
|
554
|
+
return nameRegex.IsMatch(asset.name)
|
|
555
|
+
|| (!string.IsNullOrEmpty(fileStem) && nameRegex.IsMatch(fileStem));
|
|
556
|
+
}
|
|
557
|
+
|
|
377
558
|
return asset.name.Contains(nameFilter, StringComparison.OrdinalIgnoreCase)
|
|
378
559
|
|| System.IO.Path.GetFileNameWithoutExtension(assetPath).Contains(nameFilter, StringComparison.OrdinalIgnoreCase);
|
|
379
560
|
}
|
|
@@ -421,5 +602,370 @@ namespace UCP.Bridge
|
|
|
421
602
|
return typeFilter?.Trim() ?? string.Empty;
|
|
422
603
|
}
|
|
423
604
|
|
|
605
|
+
private static Dictionary<string, object> MoveAssetInternal(string sourcePath, string destination, bool finalize)
|
|
606
|
+
{
|
|
607
|
+
var move = PrepareMove(sourcePath, destination, true);
|
|
608
|
+
return MovePreparedAsset(NormalizeMovePath(sourcePath), move, finalize);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private static Dictionary<string, object> PreviewMoveInternal(string sourcePath, string destination)
|
|
612
|
+
{
|
|
613
|
+
var normalizedSource = NormalizeMovePath(sourcePath);
|
|
614
|
+
var move = PrepareMove(normalizedSource, destination, false);
|
|
615
|
+
return DescribeMovedAsset(normalizedSource, move.ResolvedDestination, move.Changed, move.Identity);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private static ValidatedMove PrepareMove(string sourcePath, string destination, bool ensureParentFolders)
|
|
619
|
+
{
|
|
620
|
+
var normalizedSource = NormalizeMovePath(sourcePath);
|
|
621
|
+
var move = ValidateMoveRequest(normalizedSource, destination);
|
|
622
|
+
if (ensureParentFolders)
|
|
623
|
+
EnsureParentFoldersExist(move.ResolvedDestination);
|
|
624
|
+
return move;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private static Dictionary<string, object> MovePreparedAsset(string normalizedSource, ValidatedMove move, bool finalize)
|
|
628
|
+
{
|
|
629
|
+
var moveError = AssetDatabase.MoveAsset(normalizedSource, move.ResolvedDestination);
|
|
630
|
+
if (!string.IsNullOrEmpty(moveError))
|
|
631
|
+
throw new InvalidOperationException(moveError);
|
|
632
|
+
|
|
633
|
+
if (finalize)
|
|
634
|
+
{
|
|
635
|
+
AssetDatabase.SaveAssets();
|
|
636
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return DescribeMovedAsset(normalizedSource, move.ResolvedDestination, true, move.Identity);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private static Dictionary<string, object> DescribeMovedAsset(
|
|
643
|
+
string sourcePath,
|
|
644
|
+
string destinationPath,
|
|
645
|
+
bool changed,
|
|
646
|
+
AssetIdentity identity)
|
|
647
|
+
{
|
|
648
|
+
var payload = new Dictionary<string, object>
|
|
649
|
+
{
|
|
650
|
+
["status"] = "ok",
|
|
651
|
+
["sourcePath"] = sourcePath,
|
|
652
|
+
["destinationPath"] = destinationPath,
|
|
653
|
+
["changed"] = changed,
|
|
654
|
+
["guid"] = identity.Guid,
|
|
655
|
+
["isFolder"] = identity.IsFolder
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
if (!string.IsNullOrEmpty(identity.Name))
|
|
659
|
+
{
|
|
660
|
+
payload["name"] = identity.Name;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (!string.IsNullOrEmpty(identity.Type))
|
|
664
|
+
{
|
|
665
|
+
payload["type"] = identity.Type;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return payload;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private static AssetIdentity DescribeExistingAsset(string assetPath, bool isFolder)
|
|
672
|
+
{
|
|
673
|
+
var guid = GetGuidForAssetPath(assetPath);
|
|
674
|
+
var name = isFolder ? System.IO.Path.GetFileName(assetPath.TrimEnd('/')) : null;
|
|
675
|
+
var type = isFolder ? "Folder" : null;
|
|
676
|
+
|
|
677
|
+
if (!isFolder)
|
|
678
|
+
{
|
|
679
|
+
var mainAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
|
680
|
+
if (mainAsset != null)
|
|
681
|
+
{
|
|
682
|
+
name = mainAsset.name;
|
|
683
|
+
type = mainAsset.GetType().Name;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return new AssetIdentity(guid, name, type, isFolder);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private static string GetGuidForAssetPath(string assetPath)
|
|
691
|
+
{
|
|
692
|
+
var guid = AssetDatabase.AssetPathToGUID(assetPath);
|
|
693
|
+
if (!string.IsNullOrEmpty(guid))
|
|
694
|
+
return guid;
|
|
695
|
+
|
|
696
|
+
var metaPath = ResolveProjectRelativePath(assetPath + ".meta");
|
|
697
|
+
if (!File.Exists(metaPath))
|
|
698
|
+
return string.Empty;
|
|
699
|
+
|
|
700
|
+
foreach (var line in File.ReadLines(metaPath))
|
|
701
|
+
{
|
|
702
|
+
if (!line.TrimStart().StartsWith("guid:", StringComparison.Ordinal))
|
|
703
|
+
continue;
|
|
704
|
+
|
|
705
|
+
var parts = line.Split(new[] { ':' }, 2);
|
|
706
|
+
return parts.Length == 2 ? parts[1].Trim() : string.Empty;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return string.Empty;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private static string ResolveProjectRelativePath(string assetPath)
|
|
713
|
+
{
|
|
714
|
+
var projectRoot = Path.GetDirectoryName(Application.dataPath);
|
|
715
|
+
return Path.Combine(projectRoot, assetPath.Replace('/', Path.DirectorySeparatorChar));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private static string ResolveDestinationPath(string sourcePath, string destination)
|
|
719
|
+
{
|
|
720
|
+
var normalizedDestination = NormalizeMovePath(destination);
|
|
721
|
+
if (string.IsNullOrEmpty(normalizedDestination))
|
|
722
|
+
throw new ArgumentException("Missing 'destination' parameter");
|
|
723
|
+
|
|
724
|
+
if (AssetDatabase.IsValidFolder(normalizedDestination))
|
|
725
|
+
return normalizedDestination + "/" + System.IO.Path.GetFileName(sourcePath);
|
|
726
|
+
|
|
727
|
+
if (normalizedDestination.EndsWith("/", StringComparison.Ordinal))
|
|
728
|
+
return normalizedDestination.TrimEnd('/') + "/" + System.IO.Path.GetFileName(sourcePath);
|
|
729
|
+
|
|
730
|
+
return normalizedDestination;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private static void EnsureParentFoldersExist(string assetPath)
|
|
734
|
+
{
|
|
735
|
+
var parent = System.IO.Path.GetDirectoryName(assetPath)?.Replace("\\", "/");
|
|
736
|
+
if (string.IsNullOrEmpty(parent) || AssetDatabase.IsValidFolder(parent))
|
|
737
|
+
return;
|
|
738
|
+
|
|
739
|
+
CreateFoldersRecursive(parent);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private static string NormalizeMovePath(string path)
|
|
743
|
+
{
|
|
744
|
+
return path?.Trim().Replace('\\', '/');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private static ValidatedMove ValidateMoveRequest(string normalizedSource, string destination)
|
|
748
|
+
{
|
|
749
|
+
var isFolder = AssetDatabase.IsValidFolder(normalizedSource);
|
|
750
|
+
if (!isFolder && !AssetDatabase.AssetPathExists(normalizedSource))
|
|
751
|
+
throw new ArgumentException(BuildAssetNotFoundMessage(normalizedSource));
|
|
752
|
+
|
|
753
|
+
if (!IsMovableAssetPath(normalizedSource))
|
|
754
|
+
throw new ArgumentException($"Asset moves are only supported under Assets/: {normalizedSource}");
|
|
755
|
+
|
|
756
|
+
var identity = DescribeExistingAsset(normalizedSource, isFolder);
|
|
757
|
+
var resolvedDestination = ResolveDestinationPath(normalizedSource, destination);
|
|
758
|
+
if (!IsMovableAssetPath(resolvedDestination))
|
|
759
|
+
throw new ArgumentException($"Destination must be under Assets/: {resolvedDestination}");
|
|
760
|
+
|
|
761
|
+
if (string.Equals(normalizedSource, resolvedDestination, StringComparison.OrdinalIgnoreCase))
|
|
762
|
+
return new ValidatedMove(identity, resolvedDestination, false);
|
|
763
|
+
|
|
764
|
+
if (AssetDatabase.AssetPathExists(resolvedDestination) || AssetDatabase.IsValidFolder(resolvedDestination))
|
|
765
|
+
throw new ArgumentException($"Destination already exists: {resolvedDestination}");
|
|
766
|
+
|
|
767
|
+
return new ValidatedMove(identity, resolvedDestination, true);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private static bool IsMovableAssetPath(string path)
|
|
771
|
+
{
|
|
772
|
+
return !string.IsNullOrEmpty(path)
|
|
773
|
+
&& (path.Equals("Assets", StringComparison.OrdinalIgnoreCase)
|
|
774
|
+
|| path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private static string BuildAssetNotFoundMessage(string normalizedSource)
|
|
778
|
+
{
|
|
779
|
+
var message = $"Asset not found: {normalizedSource}.";
|
|
780
|
+
var suggestions = FindSimilarAssetPaths(normalizedSource, 3);
|
|
781
|
+
if (suggestions.Count > 0)
|
|
782
|
+
message += " Did you mean: " + string.Join(", ", suggestions) + "?";
|
|
783
|
+
message += " If the asset was created or renamed outside Unity, refresh or reimport so AssetDatabase can pick up the latest path.";
|
|
784
|
+
return message;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private static List<string> FindSimilarAssetPaths(string normalizedSource, int maxSuggestions)
|
|
788
|
+
{
|
|
789
|
+
var targetName = Path.GetFileNameWithoutExtension(normalizedSource) ?? normalizedSource;
|
|
790
|
+
var targetFileName = Path.GetFileName(normalizedSource) ?? normalizedSource;
|
|
791
|
+
var targetExtension = Path.GetExtension(normalizedSource) ?? string.Empty;
|
|
792
|
+
var matches = new List<(string path, int score)>();
|
|
793
|
+
|
|
794
|
+
foreach (var candidate in AssetDatabase.GetAllAssetPaths())
|
|
795
|
+
{
|
|
796
|
+
if (!candidate.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
|
797
|
+
continue;
|
|
798
|
+
|
|
799
|
+
var score = 0;
|
|
800
|
+
if (candidate.IndexOf(targetName, StringComparison.OrdinalIgnoreCase) >= 0)
|
|
801
|
+
score += 3;
|
|
802
|
+
if (string.Equals(Path.GetFileName(candidate), targetFileName, StringComparison.OrdinalIgnoreCase))
|
|
803
|
+
score += 4;
|
|
804
|
+
if (string.Equals(Path.GetExtension(candidate), targetExtension, StringComparison.OrdinalIgnoreCase))
|
|
805
|
+
score += 1;
|
|
806
|
+
if (score > 0)
|
|
807
|
+
matches.Add((candidate, score));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
matches.Sort((left, right) =>
|
|
811
|
+
{
|
|
812
|
+
var scoreCompare = right.score.CompareTo(left.score);
|
|
813
|
+
return scoreCompare != 0
|
|
814
|
+
? scoreCompare
|
|
815
|
+
: string.Compare(left.path, right.path, StringComparison.OrdinalIgnoreCase);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
var suggestions = new List<string>();
|
|
819
|
+
foreach (var match in matches)
|
|
820
|
+
{
|
|
821
|
+
if (suggestions.Contains(match.path))
|
|
822
|
+
continue;
|
|
823
|
+
suggestions.Add(match.path);
|
|
824
|
+
if (suggestions.Count >= maxSuggestions)
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return suggestions;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private static Dictionary<string, object> ParseParameters(string paramsJson)
|
|
832
|
+
{
|
|
833
|
+
return MiniJson.Deserialize(paramsJson) as Dictionary<string, object>
|
|
834
|
+
?? throw new ArgumentException("Invalid parameters");
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
private static string RequirePathParameter(Dictionary<string, object> parameters, string key)
|
|
838
|
+
{
|
|
839
|
+
if (!parameters.TryGetValue(key, out var valueObject) || valueObject == null)
|
|
840
|
+
throw new ArgumentException($"Missing '{key}' parameter");
|
|
841
|
+
|
|
842
|
+
var value = valueObject.ToString();
|
|
843
|
+
if (string.IsNullOrWhiteSpace(value))
|
|
844
|
+
throw new ArgumentException($"Missing '{key}' parameter");
|
|
845
|
+
|
|
846
|
+
return value;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private static bool TryGetOptionalBool(Dictionary<string, object> parameters, string key)
|
|
850
|
+
{
|
|
851
|
+
return parameters.TryGetValue(key, out var valueObject)
|
|
852
|
+
&& valueObject != null
|
|
853
|
+
&& Convert.ToBoolean(valueObject);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
private static List<MoveRequest> ParseMoveRequests(object movesObject)
|
|
857
|
+
{
|
|
858
|
+
var requests = new List<MoveRequest>();
|
|
859
|
+
|
|
860
|
+
if (movesObject is List<object> list)
|
|
861
|
+
{
|
|
862
|
+
foreach (var item in list)
|
|
863
|
+
{
|
|
864
|
+
if (!(item is Dictionary<string, object> entry))
|
|
865
|
+
throw new ArgumentException("Each bulk move entry must be an object");
|
|
866
|
+
|
|
867
|
+
requests.Add(new MoveRequest(
|
|
868
|
+
RequireMoveEntry(entry, "from"),
|
|
869
|
+
RequireMoveEntry(entry, "to")));
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
else if (movesObject is Dictionary<string, object> map)
|
|
873
|
+
{
|
|
874
|
+
foreach (var entry in map)
|
|
875
|
+
{
|
|
876
|
+
if (entry.Value == null)
|
|
877
|
+
throw new ArgumentException($"Missing destination for bulk move source '{entry.Key}'");
|
|
878
|
+
requests.Add(new MoveRequest(entry.Key, entry.Value.ToString()));
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
else
|
|
882
|
+
{
|
|
883
|
+
throw new ArgumentException("'moves' must be a JSON array or object map");
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return requests;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
private static string RequireMoveEntry(Dictionary<string, object> entry, string key)
|
|
890
|
+
{
|
|
891
|
+
if (!entry.TryGetValue(key, out var valueObject) || valueObject == null)
|
|
892
|
+
throw new ArgumentException($"Bulk move entry missing '{key}'");
|
|
893
|
+
|
|
894
|
+
var value = valueObject.ToString();
|
|
895
|
+
if (string.IsNullOrWhiteSpace(value))
|
|
896
|
+
throw new ArgumentException($"Bulk move entry missing '{key}'");
|
|
897
|
+
|
|
898
|
+
return value;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
private static Dictionary<string, object> AddMoveIndex(Dictionary<string, object> payload, int index)
|
|
902
|
+
{
|
|
903
|
+
payload["index"] = index;
|
|
904
|
+
return payload;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private static bool GetBool(Dictionary<string, object> payload, string key)
|
|
908
|
+
{
|
|
909
|
+
return payload.TryGetValue(key, out var valueObject)
|
|
910
|
+
&& valueObject != null
|
|
911
|
+
&& Convert.ToBoolean(valueObject);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private readonly struct MoveRequest
|
|
915
|
+
{
|
|
916
|
+
public MoveRequest(string sourcePath, string destination)
|
|
917
|
+
{
|
|
918
|
+
SourcePath = sourcePath;
|
|
919
|
+
Destination = destination;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
public string SourcePath { get; }
|
|
923
|
+
public string Destination { get; }
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private readonly struct PreparedMove
|
|
927
|
+
{
|
|
928
|
+
public PreparedMove(int index, string sourcePath, ValidatedMove move)
|
|
929
|
+
{
|
|
930
|
+
Index = index;
|
|
931
|
+
SourcePath = sourcePath;
|
|
932
|
+
Move = move;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
public int Index { get; }
|
|
936
|
+
public string SourcePath { get; }
|
|
937
|
+
public ValidatedMove Move { get; }
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private readonly struct AssetIdentity
|
|
941
|
+
{
|
|
942
|
+
public AssetIdentity(string guid, string name, string type, bool isFolder)
|
|
943
|
+
{
|
|
944
|
+
Guid = guid ?? string.Empty;
|
|
945
|
+
Name = name;
|
|
946
|
+
Type = type;
|
|
947
|
+
IsFolder = isFolder;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
public string Guid { get; }
|
|
951
|
+
public string Name { get; }
|
|
952
|
+
public string Type { get; }
|
|
953
|
+
public bool IsFolder { get; }
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private readonly struct ValidatedMove
|
|
957
|
+
{
|
|
958
|
+
public ValidatedMove(AssetIdentity identity, string resolvedDestination, bool changed)
|
|
959
|
+
{
|
|
960
|
+
Identity = identity;
|
|
961
|
+
ResolvedDestination = resolvedDestination;
|
|
962
|
+
Changed = changed;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
public AssetIdentity Identity { get; }
|
|
966
|
+
public string ResolvedDestination { get; }
|
|
967
|
+
public bool Changed { get; }
|
|
968
|
+
}
|
|
969
|
+
|
|
424
970
|
}
|
|
425
971
|
}
|