@mflrevan/ucp 0.4.5 → 0.5.0
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/Compatibility/UnityObjectCompat.cs +18 -0
- package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Compatibility.meta +8 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +345 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +6 -6
- package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +49 -33
- 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 +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/UCP.Bridge.Editor.asmdef +3 -1
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +168 -2
- package/bridge/com.ucp.bridge/package.json +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mflrevan/ucp
|
|
2
2
|
|
|
3
|
-
Version `0.
|
|
3
|
+
Version `0.5.0` 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.0";
|
|
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()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
using UnityEditor;
|
|
2
|
+
using UnityEngine;
|
|
3
|
+
|
|
4
|
+
namespace UCP.Bridge
|
|
5
|
+
{
|
|
6
|
+
internal static class UnityObjectCompat
|
|
7
|
+
{
|
|
8
|
+
public static Object ResolveByInstanceId(int instanceId)
|
|
9
|
+
{
|
|
10
|
+
return EditorUtility.InstanceIDToObject(instanceId);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public static T ResolveByInstanceId<T>(int instanceId) where T : Object
|
|
14
|
+
{
|
|
15
|
+
return ResolveByInstanceId(instanceId) as T;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
using System;
|
|
2
2
|
using System.Collections.Generic;
|
|
3
|
+
using System.IO;
|
|
3
4
|
using System.Reflection;
|
|
4
5
|
using UnityEditor;
|
|
5
6
|
using UnityEngine;
|
|
@@ -17,6 +18,8 @@ namespace UCP.Bridge
|
|
|
17
18
|
router.Register("asset/write-batch", HandleWriteScriptableObjectBatch);
|
|
18
19
|
router.Register("asset/create-so", HandleCreateScriptableObject);
|
|
19
20
|
router.Register("asset/delete", HandleDeleteAsset);
|
|
21
|
+
router.Register("asset/move", HandleMoveAsset);
|
|
22
|
+
router.Register("asset/bulk-move", HandleBulkMoveAssets);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
private static object HandleSearch(string paramsJson)
|
|
@@ -334,6 +337,87 @@ namespace UCP.Bridge
|
|
|
334
337
|
};
|
|
335
338
|
}
|
|
336
339
|
|
|
340
|
+
private static object HandleMoveAsset(string paramsJson)
|
|
341
|
+
{
|
|
342
|
+
var parameters = ParseParameters(paramsJson);
|
|
343
|
+
var sourcePath = RequirePathParameter(parameters, "path");
|
|
344
|
+
var destination = RequirePathParameter(parameters, "destination");
|
|
345
|
+
|
|
346
|
+
var result = MoveAssetInternal(sourcePath, destination, true);
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private static object HandleBulkMoveAssets(string paramsJson)
|
|
351
|
+
{
|
|
352
|
+
var parameters = ParseParameters(paramsJson);
|
|
353
|
+
if (!parameters.TryGetValue("moves", out var movesObject) || movesObject == null)
|
|
354
|
+
throw new ArgumentException("Missing 'moves' parameter");
|
|
355
|
+
|
|
356
|
+
var continueOnError = TryGetOptionalBool(parameters, "continueOnError");
|
|
357
|
+
var requests = ParseMoveRequests(movesObject);
|
|
358
|
+
var results = new List<object>();
|
|
359
|
+
var errors = new List<object>();
|
|
360
|
+
var movedCount = 0;
|
|
361
|
+
var stopped = false;
|
|
362
|
+
var anyChanged = false;
|
|
363
|
+
|
|
364
|
+
AssetDatabase.StartAssetEditing();
|
|
365
|
+
try
|
|
366
|
+
{
|
|
367
|
+
for (var index = 0; index < requests.Count; index++)
|
|
368
|
+
{
|
|
369
|
+
var request = requests[index];
|
|
370
|
+
try
|
|
371
|
+
{
|
|
372
|
+
var moveResult = MoveAssetInternal(request.SourcePath, request.Destination, false);
|
|
373
|
+
results.Add(AddMoveIndex(moveResult, index));
|
|
374
|
+
if (GetBool(moveResult, "changed"))
|
|
375
|
+
{
|
|
376
|
+
movedCount++;
|
|
377
|
+
anyChanged = true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch (Exception ex)
|
|
381
|
+
{
|
|
382
|
+
errors.Add(new Dictionary<string, object>
|
|
383
|
+
{
|
|
384
|
+
["index"] = index,
|
|
385
|
+
["sourcePath"] = request.SourcePath,
|
|
386
|
+
["destinationPath"] = request.Destination,
|
|
387
|
+
["message"] = ex.Message
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (!continueOnError)
|
|
391
|
+
{
|
|
392
|
+
stopped = true;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
finally
|
|
399
|
+
{
|
|
400
|
+
AssetDatabase.StopAssetEditing();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (anyChanged)
|
|
404
|
+
{
|
|
405
|
+
AssetDatabase.SaveAssets();
|
|
406
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return new Dictionary<string, object>
|
|
410
|
+
{
|
|
411
|
+
["status"] = "ok",
|
|
412
|
+
["requested"] = requests.Count,
|
|
413
|
+
["moved"] = movedCount,
|
|
414
|
+
["failed"] = errors.Count,
|
|
415
|
+
["stopped"] = stopped,
|
|
416
|
+
["results"] = results,
|
|
417
|
+
["errors"] = errors
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
337
421
|
private static void CreateFoldersRecursive(string path)
|
|
338
422
|
{
|
|
339
423
|
var parts = path.Replace("\\", "/").Split('/');
|
|
@@ -421,5 +505,266 @@ namespace UCP.Bridge
|
|
|
421
505
|
return typeFilter?.Trim() ?? string.Empty;
|
|
422
506
|
}
|
|
423
507
|
|
|
508
|
+
private static Dictionary<string, object> MoveAssetInternal(string sourcePath, string destination, bool finalize)
|
|
509
|
+
{
|
|
510
|
+
var normalizedSource = NormalizeMovePath(sourcePath);
|
|
511
|
+
var isFolder = AssetDatabase.IsValidFolder(normalizedSource);
|
|
512
|
+
if (!isFolder && !AssetDatabase.AssetPathExists(normalizedSource))
|
|
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
|
+
}
|
|
527
|
+
|
|
528
|
+
if (AssetDatabase.AssetPathExists(resolvedDestination) || AssetDatabase.IsValidFolder(resolvedDestination))
|
|
529
|
+
throw new ArgumentException($"Destination already exists: {resolvedDestination}");
|
|
530
|
+
|
|
531
|
+
EnsureParentFoldersExist(resolvedDestination);
|
|
532
|
+
|
|
533
|
+
var moveError = AssetDatabase.MoveAsset(normalizedSource, resolvedDestination);
|
|
534
|
+
if (!string.IsNullOrEmpty(moveError))
|
|
535
|
+
throw new InvalidOperationException(moveError);
|
|
536
|
+
|
|
537
|
+
if (finalize)
|
|
538
|
+
{
|
|
539
|
+
AssetDatabase.SaveAssets();
|
|
540
|
+
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return DescribeMovedAsset(normalizedSource, resolvedDestination, true, identity);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private static Dictionary<string, object> DescribeMovedAsset(
|
|
547
|
+
string sourcePath,
|
|
548
|
+
string destinationPath,
|
|
549
|
+
bool changed,
|
|
550
|
+
AssetIdentity identity)
|
|
551
|
+
{
|
|
552
|
+
var payload = new Dictionary<string, object>
|
|
553
|
+
{
|
|
554
|
+
["status"] = "ok",
|
|
555
|
+
["sourcePath"] = sourcePath,
|
|
556
|
+
["destinationPath"] = destinationPath,
|
|
557
|
+
["changed"] = changed,
|
|
558
|
+
["guid"] = identity.Guid,
|
|
559
|
+
["isFolder"] = identity.IsFolder
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
if (!string.IsNullOrEmpty(identity.Name))
|
|
563
|
+
{
|
|
564
|
+
payload["name"] = identity.Name;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!string.IsNullOrEmpty(identity.Type))
|
|
568
|
+
{
|
|
569
|
+
payload["type"] = identity.Type;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return payload;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private static AssetIdentity DescribeExistingAsset(string assetPath, bool isFolder)
|
|
576
|
+
{
|
|
577
|
+
var guid = GetGuidForAssetPath(assetPath);
|
|
578
|
+
var name = isFolder ? System.IO.Path.GetFileName(assetPath.TrimEnd('/')) : null;
|
|
579
|
+
var type = isFolder ? "Folder" : null;
|
|
580
|
+
|
|
581
|
+
if (!isFolder)
|
|
582
|
+
{
|
|
583
|
+
var mainAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
|
584
|
+
if (mainAsset != null)
|
|
585
|
+
{
|
|
586
|
+
name = mainAsset.name;
|
|
587
|
+
type = mainAsset.GetType().Name;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return new AssetIdentity(guid, name, type, isFolder);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private static string GetGuidForAssetPath(string assetPath)
|
|
595
|
+
{
|
|
596
|
+
var guid = AssetDatabase.AssetPathToGUID(assetPath);
|
|
597
|
+
if (!string.IsNullOrEmpty(guid))
|
|
598
|
+
return guid;
|
|
599
|
+
|
|
600
|
+
var metaPath = ResolveProjectRelativePath(assetPath + ".meta");
|
|
601
|
+
if (!File.Exists(metaPath))
|
|
602
|
+
return string.Empty;
|
|
603
|
+
|
|
604
|
+
foreach (var line in File.ReadLines(metaPath))
|
|
605
|
+
{
|
|
606
|
+
if (!line.TrimStart().StartsWith("guid:", StringComparison.Ordinal))
|
|
607
|
+
continue;
|
|
608
|
+
|
|
609
|
+
var parts = line.Split(new[] { ':' }, 2);
|
|
610
|
+
return parts.Length == 2 ? parts[1].Trim() : string.Empty;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return string.Empty;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private static string ResolveProjectRelativePath(string assetPath)
|
|
617
|
+
{
|
|
618
|
+
var projectRoot = Path.GetDirectoryName(Application.dataPath);
|
|
619
|
+
return Path.Combine(projectRoot, assetPath.Replace('/', Path.DirectorySeparatorChar));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private static string ResolveDestinationPath(string sourcePath, string destination)
|
|
623
|
+
{
|
|
624
|
+
var normalizedDestination = NormalizeMovePath(destination);
|
|
625
|
+
if (string.IsNullOrEmpty(normalizedDestination))
|
|
626
|
+
throw new ArgumentException("Missing 'destination' parameter");
|
|
627
|
+
|
|
628
|
+
if (AssetDatabase.IsValidFolder(normalizedDestination))
|
|
629
|
+
return normalizedDestination + "/" + System.IO.Path.GetFileName(sourcePath);
|
|
630
|
+
|
|
631
|
+
if (normalizedDestination.EndsWith("/", StringComparison.Ordinal))
|
|
632
|
+
return normalizedDestination.TrimEnd('/') + "/" + System.IO.Path.GetFileName(sourcePath);
|
|
633
|
+
|
|
634
|
+
return normalizedDestination;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private static void EnsureParentFoldersExist(string assetPath)
|
|
638
|
+
{
|
|
639
|
+
var parent = System.IO.Path.GetDirectoryName(assetPath)?.Replace("\\", "/");
|
|
640
|
+
if (string.IsNullOrEmpty(parent) || AssetDatabase.IsValidFolder(parent))
|
|
641
|
+
return;
|
|
642
|
+
|
|
643
|
+
CreateFoldersRecursive(parent);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private static string NormalizeMovePath(string path)
|
|
647
|
+
{
|
|
648
|
+
return path?.Trim().Replace('\\', '/');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private static bool IsMovableAssetPath(string path)
|
|
652
|
+
{
|
|
653
|
+
return !string.IsNullOrEmpty(path)
|
|
654
|
+
&& (path.Equals("Assets", StringComparison.OrdinalIgnoreCase)
|
|
655
|
+
|| path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private static Dictionary<string, object> ParseParameters(string paramsJson)
|
|
659
|
+
{
|
|
660
|
+
return MiniJson.Deserialize(paramsJson) as Dictionary<string, object>
|
|
661
|
+
?? throw new ArgumentException("Invalid parameters");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private static string RequirePathParameter(Dictionary<string, object> parameters, string key)
|
|
665
|
+
{
|
|
666
|
+
if (!parameters.TryGetValue(key, out var valueObject) || valueObject == null)
|
|
667
|
+
throw new ArgumentException($"Missing '{key}' parameter");
|
|
668
|
+
|
|
669
|
+
var value = valueObject.ToString();
|
|
670
|
+
if (string.IsNullOrWhiteSpace(value))
|
|
671
|
+
throw new ArgumentException($"Missing '{key}' parameter");
|
|
672
|
+
|
|
673
|
+
return value;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private static bool TryGetOptionalBool(Dictionary<string, object> parameters, string key)
|
|
677
|
+
{
|
|
678
|
+
return parameters.TryGetValue(key, out var valueObject)
|
|
679
|
+
&& valueObject != null
|
|
680
|
+
&& Convert.ToBoolean(valueObject);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private static List<MoveRequest> ParseMoveRequests(object movesObject)
|
|
684
|
+
{
|
|
685
|
+
var requests = new List<MoveRequest>();
|
|
686
|
+
|
|
687
|
+
if (movesObject is List<object> list)
|
|
688
|
+
{
|
|
689
|
+
foreach (var item in list)
|
|
690
|
+
{
|
|
691
|
+
if (!(item is Dictionary<string, object> entry))
|
|
692
|
+
throw new ArgumentException("Each bulk move entry must be an object");
|
|
693
|
+
|
|
694
|
+
requests.Add(new MoveRequest(
|
|
695
|
+
RequireMoveEntry(entry, "from"),
|
|
696
|
+
RequireMoveEntry(entry, "to")));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
else if (movesObject is Dictionary<string, object> map)
|
|
700
|
+
{
|
|
701
|
+
foreach (var entry in map)
|
|
702
|
+
{
|
|
703
|
+
if (entry.Value == null)
|
|
704
|
+
throw new ArgumentException($"Missing destination for bulk move source '{entry.Key}'");
|
|
705
|
+
requests.Add(new MoveRequest(entry.Key, entry.Value.ToString()));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
else
|
|
709
|
+
{
|
|
710
|
+
throw new ArgumentException("'moves' must be a JSON array or object map");
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return requests;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
private static string RequireMoveEntry(Dictionary<string, object> entry, string key)
|
|
717
|
+
{
|
|
718
|
+
if (!entry.TryGetValue(key, out var valueObject) || valueObject == null)
|
|
719
|
+
throw new ArgumentException($"Bulk move entry missing '{key}'");
|
|
720
|
+
|
|
721
|
+
var value = valueObject.ToString();
|
|
722
|
+
if (string.IsNullOrWhiteSpace(value))
|
|
723
|
+
throw new ArgumentException($"Bulk move entry missing '{key}'");
|
|
724
|
+
|
|
725
|
+
return value;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private static Dictionary<string, object> AddMoveIndex(Dictionary<string, object> payload, int index)
|
|
729
|
+
{
|
|
730
|
+
payload["index"] = index;
|
|
731
|
+
return payload;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private static bool GetBool(Dictionary<string, object> payload, string key)
|
|
735
|
+
{
|
|
736
|
+
return payload.TryGetValue(key, out var valueObject)
|
|
737
|
+
&& valueObject != null
|
|
738
|
+
&& Convert.ToBoolean(valueObject);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private readonly struct MoveRequest
|
|
742
|
+
{
|
|
743
|
+
public MoveRequest(string sourcePath, string destination)
|
|
744
|
+
{
|
|
745
|
+
SourcePath = sourcePath;
|
|
746
|
+
Destination = destination;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
public string SourcePath { get; }
|
|
750
|
+
public string Destination { get; }
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private readonly struct AssetIdentity
|
|
754
|
+
{
|
|
755
|
+
public AssetIdentity(string guid, string name, string type, bool isFolder)
|
|
756
|
+
{
|
|
757
|
+
Guid = guid ?? string.Empty;
|
|
758
|
+
Name = name;
|
|
759
|
+
Type = type;
|
|
760
|
+
IsFolder = isFolder;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
public string Guid { get; }
|
|
764
|
+
public string Name { get; }
|
|
765
|
+
public string Type { get; }
|
|
766
|
+
public bool IsFolder { get; }
|
|
767
|
+
}
|
|
768
|
+
|
|
424
769
|
}
|
|
425
770
|
}
|
|
@@ -295,7 +295,7 @@ namespace UCP.Bridge
|
|
|
295
295
|
|
|
296
296
|
private static GameObject FindGameObject(int instanceId)
|
|
297
297
|
{
|
|
298
|
-
var obj =
|
|
298
|
+
var obj = UnityObjectCompat.ResolveByInstanceId<GameObject>(instanceId);
|
|
299
299
|
if (obj != null) return obj;
|
|
300
300
|
|
|
301
301
|
for (int i = 0; i < SceneManager.sceneCount; i++)
|
|
@@ -228,7 +228,7 @@ namespace UCP.Bridge
|
|
|
228
228
|
if (p.TryGetValue("instanceId", out var idObj))
|
|
229
229
|
{
|
|
230
230
|
int instanceId = Convert.ToInt32(idObj);
|
|
231
|
-
var obj =
|
|
231
|
+
var obj = UnityObjectCompat.ResolveByInstanceId(instanceId);
|
|
232
232
|
|
|
233
233
|
if (obj is Material directMat)
|
|
234
234
|
return directMat;
|
|
@@ -330,7 +330,7 @@ namespace UCP.Bridge
|
|
|
330
330
|
}
|
|
331
331
|
else if (texDict.TryGetValue("instanceId", out var tid))
|
|
332
332
|
{
|
|
333
|
-
var tex =
|
|
333
|
+
var tex = UnityObjectCompat.ResolveByInstanceId<Texture>(Convert.ToInt32(tid));
|
|
334
334
|
mat.SetTexture(propName, tex);
|
|
335
335
|
}
|
|
336
336
|
}
|
|
@@ -65,7 +65,7 @@ namespace UCP.Bridge
|
|
|
65
65
|
|
|
66
66
|
private static UnityEngine.Object ResolveByInstanceId(int instanceId, string propertyName)
|
|
67
67
|
{
|
|
68
|
-
var resolved =
|
|
68
|
+
var resolved = UnityObjectCompat.ResolveByInstanceId(instanceId);
|
|
69
69
|
if (resolved == null)
|
|
70
70
|
throw new ArgumentException($"Object reference for '{propertyName}' could not resolve instance id {instanceId}");
|
|
71
71
|
|
|
@@ -26,7 +26,7 @@ namespace UCP.Bridge
|
|
|
26
26
|
if (p == null || !p.TryGetValue("instanceId", out var idObj))
|
|
27
27
|
throw new ArgumentException("Missing 'instanceId' parameter");
|
|
28
28
|
|
|
29
|
-
var go =
|
|
29
|
+
var go = UnityObjectCompat.ResolveByInstanceId<GameObject>(Convert.ToInt32(idObj));
|
|
30
30
|
if (go == null)
|
|
31
31
|
throw new ArgumentException($"GameObject not found: {idObj}");
|
|
32
32
|
|
|
@@ -63,7 +63,7 @@ namespace UCP.Bridge
|
|
|
63
63
|
if (p == null || !p.TryGetValue("instanceId", out var idObj))
|
|
64
64
|
throw new ArgumentException("Missing 'instanceId' parameter");
|
|
65
65
|
|
|
66
|
-
var go =
|
|
66
|
+
var go = UnityObjectCompat.ResolveByInstanceId<GameObject>(Convert.ToInt32(idObj));
|
|
67
67
|
if (go == null)
|
|
68
68
|
throw new ArgumentException($"GameObject not found: {idObj}");
|
|
69
69
|
|
|
@@ -88,7 +88,7 @@ namespace UCP.Bridge
|
|
|
88
88
|
if (p == null || !p.TryGetValue("instanceId", out var idObj))
|
|
89
89
|
throw new ArgumentException("Missing 'instanceId' parameter");
|
|
90
90
|
|
|
91
|
-
var go =
|
|
91
|
+
var go = UnityObjectCompat.ResolveByInstanceId<GameObject>(Convert.ToInt32(idObj));
|
|
92
92
|
if (go == null)
|
|
93
93
|
throw new ArgumentException($"GameObject not found: {idObj}");
|
|
94
94
|
|
|
@@ -116,7 +116,7 @@ namespace UCP.Bridge
|
|
|
116
116
|
if (p.TryGetValue("completely", out var compObj))
|
|
117
117
|
completely = Convert.ToBoolean(compObj);
|
|
118
118
|
|
|
119
|
-
var go =
|
|
119
|
+
var go = UnityObjectCompat.ResolveByInstanceId<GameObject>(Convert.ToInt32(idObj));
|
|
120
120
|
if (go == null)
|
|
121
121
|
throw new ArgumentException($"GameObject not found: {idObj}");
|
|
122
122
|
|
|
@@ -147,7 +147,7 @@ namespace UCP.Bridge
|
|
|
147
147
|
if (!p.TryGetValue("path", out var pathObj))
|
|
148
148
|
throw new ArgumentException("Missing 'path' parameter");
|
|
149
149
|
|
|
150
|
-
var go =
|
|
150
|
+
var go = UnityObjectCompat.ResolveByInstanceId<GameObject>(Convert.ToInt32(idObj));
|
|
151
151
|
if (go == null)
|
|
152
152
|
throw new ArgumentException($"GameObject not found: {idObj}");
|
|
153
153
|
|
|
@@ -191,7 +191,7 @@ namespace UCP.Bridge
|
|
|
191
191
|
if (p == null || !p.TryGetValue("instanceId", out var idObj))
|
|
192
192
|
throw new ArgumentException("Missing 'instanceId' parameter");
|
|
193
193
|
|
|
194
|
-
var go =
|
|
194
|
+
var go = UnityObjectCompat.ResolveByInstanceId<GameObject>(Convert.ToInt32(idObj));
|
|
195
195
|
if (go == null)
|
|
196
196
|
throw new ArgumentException($"GameObject not found: {idObj}");
|
|
197
197
|
|
|
@@ -179,33 +179,40 @@ namespace UCP.Bridge
|
|
|
179
179
|
|
|
180
180
|
// Use SerializedObject for reliable Unity property enumeration
|
|
181
181
|
var so = new SerializedObject(comp);
|
|
182
|
-
|
|
183
|
-
if (prop.NextVisible(true))
|
|
182
|
+
try
|
|
184
183
|
{
|
|
185
|
-
|
|
184
|
+
so.Update();
|
|
185
|
+
var prop = so.GetIterator();
|
|
186
|
+
if (prop.NextVisible(true))
|
|
186
187
|
{
|
|
187
|
-
|
|
188
|
+
do
|
|
188
189
|
{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
190
|
+
var fieldDict = new Dictionary<string, object>
|
|
191
|
+
{
|
|
192
|
+
["name"] = prop.name,
|
|
193
|
+
["displayName"] = prop.displayName,
|
|
194
|
+
["type"] = prop.propertyType.ToString(),
|
|
195
|
+
["editable"] = !prop.isReadOnly(true)
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
try
|
|
199
|
+
{
|
|
200
|
+
fieldDict["value"] = SerializedPropertyToValue(prop);
|
|
201
|
+
}
|
|
202
|
+
catch
|
|
203
|
+
{
|
|
204
|
+
fieldDict["value"] = "<unreadable>";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fields.Add(fieldDict);
|
|
202
208
|
}
|
|
203
|
-
|
|
204
|
-
fields.Add(fieldDict);
|
|
209
|
+
while (prop.NextVisible(false));
|
|
205
210
|
}
|
|
206
|
-
while (prop.NextVisible(false));
|
|
207
211
|
}
|
|
208
|
-
|
|
212
|
+
finally
|
|
213
|
+
{
|
|
214
|
+
so.Dispose();
|
|
215
|
+
}
|
|
209
216
|
|
|
210
217
|
return fields;
|
|
211
218
|
}
|
|
@@ -273,14 +280,17 @@ namespace UCP.Bridge
|
|
|
273
280
|
private static object GetPropertyValue(Component comp, string propertyName)
|
|
274
281
|
{
|
|
275
282
|
var so = new SerializedObject(comp);
|
|
276
|
-
|
|
277
|
-
|
|
283
|
+
try
|
|
284
|
+
{
|
|
285
|
+
so.Update();
|
|
286
|
+
var prop = so.FindProperty(propertyName);
|
|
287
|
+
if (prop != null)
|
|
288
|
+
return SerializedPropertyToValue(prop);
|
|
289
|
+
}
|
|
290
|
+
finally
|
|
278
291
|
{
|
|
279
|
-
var val = SerializedPropertyToValue(prop);
|
|
280
292
|
so.Dispose();
|
|
281
|
-
return val;
|
|
282
293
|
}
|
|
283
|
-
so.Dispose();
|
|
284
294
|
|
|
285
295
|
// Fallback to reflection
|
|
286
296
|
var type = comp.GetType();
|
|
@@ -298,15 +308,21 @@ namespace UCP.Bridge
|
|
|
298
308
|
private static void SetPropertyValue(Component comp, string propertyName, object jsonValue)
|
|
299
309
|
{
|
|
300
310
|
var so = new SerializedObject(comp);
|
|
301
|
-
|
|
302
|
-
|
|
311
|
+
try
|
|
312
|
+
{
|
|
313
|
+
so.Update();
|
|
314
|
+
var prop = so.FindProperty(propertyName);
|
|
315
|
+
if (prop != null)
|
|
316
|
+
{
|
|
317
|
+
SetSerializedPropertyValue(prop, jsonValue);
|
|
318
|
+
so.ApplyModifiedProperties();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
finally
|
|
303
323
|
{
|
|
304
|
-
SetSerializedPropertyValue(prop, jsonValue);
|
|
305
|
-
so.ApplyModifiedProperties();
|
|
306
324
|
so.Dispose();
|
|
307
|
-
return;
|
|
308
325
|
}
|
|
309
|
-
so.Dispose();
|
|
310
326
|
|
|
311
327
|
// Fallback to reflection
|
|
312
328
|
var type = comp.GetType();
|
|
@@ -502,7 +518,7 @@ namespace UCP.Bridge
|
|
|
502
518
|
|
|
503
519
|
private static GameObject FindGameObject(int instanceId)
|
|
504
520
|
{
|
|
505
|
-
var obj =
|
|
521
|
+
var obj = UnityObjectCompat.ResolveByInstanceId<GameObject>(instanceId);
|
|
506
522
|
if (obj != null) return obj;
|
|
507
523
|
|
|
508
524
|
// Search in loaded scenes
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using UnityEditor;
|
|
4
|
+
using UnityEngine;
|
|
5
|
+
|
|
6
|
+
namespace UCP.Bridge
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// Bridge controller for reference search - used as a fallback when native Rust
|
|
10
|
+
/// indexing is not available (non-Force-Text projects) or for correctness verification.
|
|
11
|
+
/// </summary>
|
|
12
|
+
public static class ReferenceController
|
|
13
|
+
{
|
|
14
|
+
public static void Register(CommandRouter router)
|
|
15
|
+
{
|
|
16
|
+
router.Register("references/find", HandleFind);
|
|
17
|
+
router.Register("references/serialization-status", HandleSerializationStatus);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private static object HandleSerializationStatus(string paramsJson)
|
|
21
|
+
{
|
|
22
|
+
var settings = new SerializedObject(
|
|
23
|
+
AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/EditorSettings.asset")[0]);
|
|
24
|
+
try
|
|
25
|
+
{
|
|
26
|
+
settings.Update();
|
|
27
|
+
var modeProp = settings.FindProperty("m_SerializationMode");
|
|
28
|
+
int mode = modeProp != null ? modeProp.intValue : -1;
|
|
29
|
+
|
|
30
|
+
return new Dictionary<string, object>
|
|
31
|
+
{
|
|
32
|
+
{ "serializationMode", mode },
|
|
33
|
+
{ "forceText", mode == 2 },
|
|
34
|
+
{ "visibleMetaFiles", EditorSettings.externalVersionControl == "Visible Meta Files" }
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
finally
|
|
38
|
+
{
|
|
39
|
+
settings.Dispose();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private static object HandleFind(string paramsJson)
|
|
44
|
+
{
|
|
45
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
46
|
+
if (p == null)
|
|
47
|
+
throw new ArgumentException("Invalid parameters");
|
|
48
|
+
|
|
49
|
+
string targetGuid = null;
|
|
50
|
+
long targetFileId = 0;
|
|
51
|
+
int maxResults = 100;
|
|
52
|
+
|
|
53
|
+
if (p.TryGetValue("guid", out var gObj) && gObj != null)
|
|
54
|
+
targetGuid = gObj.ToString();
|
|
55
|
+
if (p.TryGetValue("fileId", out var fObj) && fObj != null)
|
|
56
|
+
targetFileId = Convert.ToInt64(fObj);
|
|
57
|
+
if (p.TryGetValue("maxResults", out var mObj) && mObj != null)
|
|
58
|
+
maxResults = Convert.ToInt32(mObj);
|
|
59
|
+
|
|
60
|
+
if (string.IsNullOrEmpty(targetGuid))
|
|
61
|
+
throw new ArgumentException("Missing 'guid' parameter");
|
|
62
|
+
|
|
63
|
+
var targetPath = AssetDatabase.GUIDToAssetPath(targetGuid);
|
|
64
|
+
var references = new List<object>();
|
|
65
|
+
|
|
66
|
+
// Phase 1: Find all assets that depend on the target using AssetDatabase
|
|
67
|
+
var allAssets = AssetDatabase.GetAllAssetPaths();
|
|
68
|
+
var dependentPaths = new List<string>();
|
|
69
|
+
|
|
70
|
+
foreach (var assetPath in allAssets)
|
|
71
|
+
{
|
|
72
|
+
if (!assetPath.StartsWith("Assets/") && !assetPath.StartsWith("Packages/"))
|
|
73
|
+
continue;
|
|
74
|
+
|
|
75
|
+
var deps = AssetDatabase.GetDependencies(assetPath, false);
|
|
76
|
+
foreach (var dep in deps)
|
|
77
|
+
{
|
|
78
|
+
if (dep == targetPath)
|
|
79
|
+
{
|
|
80
|
+
dependentPaths.Add(assetPath);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Phase 2: Walk SerializedObject properties to find exact reference locations
|
|
87
|
+
foreach (var sourcePath in dependentPaths)
|
|
88
|
+
{
|
|
89
|
+
if (references.Count >= maxResults)
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
var assets = AssetDatabase.LoadAllAssetsAtPath(sourcePath);
|
|
93
|
+
foreach (var asset in assets)
|
|
94
|
+
{
|
|
95
|
+
if (asset == null) continue;
|
|
96
|
+
if (references.Count >= maxResults) break;
|
|
97
|
+
|
|
98
|
+
var so = new SerializedObject(asset);
|
|
99
|
+
try
|
|
100
|
+
{
|
|
101
|
+
so.Update();
|
|
102
|
+
var iterator = so.GetIterator();
|
|
103
|
+
bool enterChildren = true;
|
|
104
|
+
|
|
105
|
+
while (iterator.Next(enterChildren))
|
|
106
|
+
{
|
|
107
|
+
enterChildren = true;
|
|
108
|
+
|
|
109
|
+
if (iterator.propertyType != SerializedPropertyType.ObjectReference)
|
|
110
|
+
continue;
|
|
111
|
+
|
|
112
|
+
var refValue = iterator.objectReferenceValue;
|
|
113
|
+
if (refValue == null) continue;
|
|
114
|
+
|
|
115
|
+
string refPath = AssetDatabase.GetAssetPath(refValue);
|
|
116
|
+
if (string.IsNullOrEmpty(refPath)) continue;
|
|
117
|
+
|
|
118
|
+
string refGuid = AssetDatabase.AssetPathToGUID(refPath);
|
|
119
|
+
if (refGuid != targetGuid) continue;
|
|
120
|
+
|
|
121
|
+
// If a specific fileId was requested, check it
|
|
122
|
+
if (targetFileId != 0)
|
|
123
|
+
{
|
|
124
|
+
if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(
|
|
125
|
+
refValue, out string _, out long localId))
|
|
126
|
+
continue;
|
|
127
|
+
if (localId != targetFileId)
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
references.Add(new Dictionary<string, object>
|
|
132
|
+
{
|
|
133
|
+
{ "sourcePath", sourcePath },
|
|
134
|
+
{ "sourceObjectName", asset.name },
|
|
135
|
+
{ "sourceObjectType", asset.GetType().Name },
|
|
136
|
+
{ "propertyPath", iterator.propertyPath },
|
|
137
|
+
{ "targetGuid", targetGuid },
|
|
138
|
+
{ "targetPath", targetPath },
|
|
139
|
+
{ "referencedObjectName", refValue.name },
|
|
140
|
+
{ "referencedObjectType", refValue.GetType().Name }
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (references.Count >= maxResults)
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally
|
|
148
|
+
{
|
|
149
|
+
so.Dispose();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return new Dictionary<string, object>
|
|
155
|
+
{
|
|
156
|
+
{ "targetGuid", targetGuid },
|
|
157
|
+
{ "targetPath", targetPath ?? "" },
|
|
158
|
+
{ "count", references.Count },
|
|
159
|
+
{ "references", references }
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -273,7 +273,7 @@ namespace UCP.Bridge
|
|
|
273
273
|
|
|
274
274
|
private static GameObject FindGameObject(int instanceId)
|
|
275
275
|
{
|
|
276
|
-
var direct =
|
|
276
|
+
var direct = UnityObjectCompat.ResolveByInstanceId<GameObject>(instanceId);
|
|
277
277
|
if (direct != null)
|
|
278
278
|
return direct;
|
|
279
279
|
|
|
@@ -21,6 +21,12 @@ namespace UCP.Bridge.Tests
|
|
|
21
21
|
private const string TempTextPath = "Assets/UcpControllerSmoke.txt";
|
|
22
22
|
private const string TempScriptPath = "Assets/UcpControllerSmokeComponent.cs";
|
|
23
23
|
private const string TempTexturePath = "Assets/UcpImporterSmoke.png";
|
|
24
|
+
private const string TempScenePath = "Assets/UcpControllerMoveScene.unity";
|
|
25
|
+
private const string TempMovedFolderPath = "Assets/UcpControllerMoved";
|
|
26
|
+
private const string TempMovedAssetPath = "Assets/UcpControllerMoved/UcpControllerSmoke.asset";
|
|
27
|
+
private const string TempMovedReferenceAssetPath = "Assets/UcpControllerMoved/UcpControllerReference.asset";
|
|
28
|
+
private const string TempMovedMaterialPath = "Assets/UcpControllerMoved/UcpControllerSmoke.mat";
|
|
29
|
+
private const string TempMovedScenePath = "Assets/UcpControllerMoved/UcpControllerMoveScene.unity";
|
|
24
30
|
private const string TempProfilerExportPath = "ProfilerCaptures\\smoke-export.json";
|
|
25
31
|
private const string TempLocalPackageFolder = "TempUcpLocalPackage";
|
|
26
32
|
private const string TempLocalPackageName = "com.ucp.temp.local";
|
|
@@ -53,6 +59,8 @@ namespace UCP.Bridge.Tests
|
|
|
53
59
|
DeleteTempTextFile();
|
|
54
60
|
DeleteTempScriptFile();
|
|
55
61
|
DeleteTempTextureAsset();
|
|
62
|
+
DeleteTempScene();
|
|
63
|
+
DeleteTempMovedFolder();
|
|
56
64
|
DeleteTempProfilerExport();
|
|
57
65
|
DeleteTempLocalPackage();
|
|
58
66
|
RemoveTempLocalPackageDependencyIfPresent();
|
|
@@ -74,6 +82,8 @@ namespace UCP.Bridge.Tests
|
|
|
74
82
|
DeleteTempTextFile();
|
|
75
83
|
DeleteTempScriptFile();
|
|
76
84
|
DeleteTempTextureAsset();
|
|
85
|
+
DeleteTempScene();
|
|
86
|
+
DeleteTempMovedFolder();
|
|
77
87
|
DeleteTempProfilerExport();
|
|
78
88
|
DeleteTempLocalPackage();
|
|
79
89
|
RemoveTempLocalPackageDependencyIfPresent();
|
|
@@ -371,7 +381,7 @@ namespace UCP.Bridge.Tests
|
|
|
371
381
|
);
|
|
372
382
|
Assert.That(getPosition.error, Is.Null);
|
|
373
383
|
|
|
374
|
-
var updated = EditorUtility.
|
|
384
|
+
var updated = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
|
|
375
385
|
Assert.That(updated, Is.Not.Null);
|
|
376
386
|
var localPosition = updated.transform.localPosition;
|
|
377
387
|
Assert.That(localPosition.x, Is.EqualTo(1f).Within(0.001f));
|
|
@@ -383,7 +393,7 @@ namespace UCP.Bridge.Tests
|
|
|
383
393
|
|
|
384
394
|
var delete = _router.Dispatch("object/delete", 1, "{\"instanceId\":" + instanceId + "}");
|
|
385
395
|
Assert.That(delete.error, Is.Null);
|
|
386
|
-
Assert.That(EditorUtility.
|
|
396
|
+
Assert.That(EditorUtility.InstanceIDToObject(instanceId), Is.Null);
|
|
387
397
|
}
|
|
388
398
|
|
|
389
399
|
[Test]
|
|
@@ -477,6 +487,144 @@ namespace UCP.Bridge.Tests
|
|
|
477
487
|
Assert.That(AssetDatabase.GetAssetPath(reloaded.referenceAsset), Is.EqualTo(TempReferenceAssetPath));
|
|
478
488
|
}
|
|
479
489
|
|
|
490
|
+
[Test]
|
|
491
|
+
public void AssetMove_PreservesCustomAssetReferenceAndGuid()
|
|
492
|
+
{
|
|
493
|
+
var reference = ScriptableObject.CreateInstance<SearchRootAsset>();
|
|
494
|
+
reference.name = "MoveTarget";
|
|
495
|
+
AssetDatabase.CreateAsset(reference, TempReferenceAssetPath);
|
|
496
|
+
|
|
497
|
+
var asset = ScriptableObject.CreateInstance<BatchWritableAsset>();
|
|
498
|
+
asset.referenceAsset = reference;
|
|
499
|
+
AssetDatabase.CreateAsset(asset, TempAssetPath);
|
|
500
|
+
AssetDatabase.SaveAssets();
|
|
501
|
+
|
|
502
|
+
var originalGuid = AssetDatabase.AssetPathToGUID(TempReferenceAssetPath);
|
|
503
|
+
|
|
504
|
+
var response = _router.Dispatch(
|
|
505
|
+
"asset/move",
|
|
506
|
+
1,
|
|
507
|
+
"{\"path\":\"" + TempReferenceAssetPath + "\",\"destination\":\"" + TempMovedReferenceAssetPath + "\"}");
|
|
508
|
+
|
|
509
|
+
Assert.That(response.error, Is.Null);
|
|
510
|
+
|
|
511
|
+
var result = (Dictionary<string, object>)response.result;
|
|
512
|
+
Assert.That(Convert.ToBoolean(result["changed"]), Is.True);
|
|
513
|
+
Assert.That(result["sourcePath"], Is.EqualTo(TempReferenceAssetPath));
|
|
514
|
+
Assert.That(result["destinationPath"], Is.EqualTo(TempMovedReferenceAssetPath));
|
|
515
|
+
Assert.That(result["guid"], Is.EqualTo(originalGuid));
|
|
516
|
+
|
|
517
|
+
var reloaded = AssetDatabase.LoadAssetAtPath<BatchWritableAsset>(TempAssetPath);
|
|
518
|
+
Assert.That(reloaded, Is.Not.Null);
|
|
519
|
+
Assert.That(reloaded.referenceAsset, Is.Not.Null);
|
|
520
|
+
Assert.That(AssetDatabase.GetAssetPath(reloaded.referenceAsset), Is.EqualTo(TempMovedReferenceAssetPath));
|
|
521
|
+
Assert.That(AssetDatabase.AssetPathToGUID(TempMovedReferenceAssetPath), Is.EqualTo(originalGuid));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
[Test]
|
|
525
|
+
public void AssetBulkMove_MovesMultipleAssetsAndPreservesReferences()
|
|
526
|
+
{
|
|
527
|
+
var reference = ScriptableObject.CreateInstance<SearchRootAsset>();
|
|
528
|
+
reference.name = "BulkMoveRef";
|
|
529
|
+
AssetDatabase.CreateAsset(reference, TempReferenceAssetPath);
|
|
530
|
+
|
|
531
|
+
var asset = ScriptableObject.CreateInstance<BatchWritableAsset>();
|
|
532
|
+
asset.referenceAsset = reference;
|
|
533
|
+
asset.maxPlayers = 7;
|
|
534
|
+
AssetDatabase.CreateAsset(asset, TempAssetPath);
|
|
535
|
+
AssetDatabase.SaveAssets();
|
|
536
|
+
|
|
537
|
+
var response = _router.Dispatch(
|
|
538
|
+
"asset/bulk-move",
|
|
539
|
+
1,
|
|
540
|
+
"{\"moves\":["
|
|
541
|
+
+ "{\"from\":\"" + TempReferenceAssetPath + "\",\"to\":\"" + TempMovedReferenceAssetPath + "\"},"
|
|
542
|
+
+ "{\"from\":\"" + TempAssetPath + "\",\"to\":\"" + TempMovedAssetPath + "\"}"
|
|
543
|
+
+ "]}");
|
|
544
|
+
|
|
545
|
+
Assert.That(response.error, Is.Null);
|
|
546
|
+
|
|
547
|
+
var result = (Dictionary<string, object>)response.result;
|
|
548
|
+
Assert.That(Convert.ToInt32(result["requested"]), Is.EqualTo(2));
|
|
549
|
+
Assert.That(Convert.ToInt32(result["moved"]), Is.EqualTo(2));
|
|
550
|
+
Assert.That(Convert.ToInt32(result["failed"]), Is.EqualTo(0));
|
|
551
|
+
|
|
552
|
+
var movedAsset = AssetDatabase.LoadAssetAtPath<BatchWritableAsset>(TempMovedAssetPath);
|
|
553
|
+
Assert.That(movedAsset, Is.Not.Null);
|
|
554
|
+
Assert.That(movedAsset.maxPlayers, Is.EqualTo(7));
|
|
555
|
+
Assert.That(movedAsset.referenceAsset, Is.Not.Null);
|
|
556
|
+
Assert.That(AssetDatabase.GetAssetPath(movedAsset.referenceAsset), Is.EqualTo(TempMovedReferenceAssetPath));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
[Test]
|
|
560
|
+
public void AssetMove_PreservesSceneMaterialReference()
|
|
561
|
+
{
|
|
562
|
+
var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
|
|
563
|
+
Assert.That(shader, Is.Not.Null);
|
|
564
|
+
|
|
565
|
+
var material = new Material(shader) { name = "MoveSceneMaterial" };
|
|
566
|
+
AssetDatabase.CreateAsset(material, TempMaterialPath);
|
|
567
|
+
AssetDatabase.SaveAssets();
|
|
568
|
+
|
|
569
|
+
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
570
|
+
var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
|
571
|
+
cube.name = "MoveSceneCube";
|
|
572
|
+
cube.GetComponent<MeshRenderer>().sharedMaterial = material;
|
|
573
|
+
Assert.That(EditorSceneManager.SaveScene(scene, TempScenePath), Is.True);
|
|
574
|
+
|
|
575
|
+
var response = _router.Dispatch(
|
|
576
|
+
"asset/move",
|
|
577
|
+
1,
|
|
578
|
+
"{\"path\":\"" + TempMaterialPath + "\",\"destination\":\"" + TempMovedMaterialPath + "\"}");
|
|
579
|
+
|
|
580
|
+
Assert.That(response.error, Is.Null);
|
|
581
|
+
|
|
582
|
+
EditorSceneManager.OpenScene(TempScenePath, OpenSceneMode.Single);
|
|
583
|
+
var movedCube = GameObject.Find("MoveSceneCube");
|
|
584
|
+
Assert.That(movedCube, Is.Not.Null);
|
|
585
|
+
var renderer = movedCube.GetComponent<MeshRenderer>();
|
|
586
|
+
Assert.That(renderer, Is.Not.Null);
|
|
587
|
+
Assert.That(renderer.sharedMaterial, Is.Not.Null);
|
|
588
|
+
Assert.That(AssetDatabase.GetAssetPath(renderer.sharedMaterial), Is.EqualTo(TempMovedMaterialPath));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
[Test]
|
|
592
|
+
public void AssetMove_UpdatesBuildSettingsWhenSceneMoves()
|
|
593
|
+
{
|
|
594
|
+
var originalScenes = EditorBuildSettings.scenes;
|
|
595
|
+
try
|
|
596
|
+
{
|
|
597
|
+
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
598
|
+
var marker = new GameObject("MovedSceneMarker");
|
|
599
|
+
marker.transform.position = new Vector3(1f, 2f, 3f);
|
|
600
|
+
Assert.That(EditorSceneManager.SaveScene(scene, TempScenePath), Is.True);
|
|
601
|
+
|
|
602
|
+
var originalGuid = AssetDatabase.AssetPathToGUID(TempScenePath);
|
|
603
|
+
EditorBuildSettings.scenes = new[]
|
|
604
|
+
{
|
|
605
|
+
new EditorBuildSettingsScene(TempScenePath, true)
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
var response = _router.Dispatch(
|
|
609
|
+
"asset/move",
|
|
610
|
+
1,
|
|
611
|
+
"{\"path\":\"" + TempScenePath + "\",\"destination\":\"" + TempMovedScenePath + "\"}");
|
|
612
|
+
|
|
613
|
+
Assert.That(response.error, Is.Null);
|
|
614
|
+
Assert.That(AssetDatabase.AssetPathToGUID(TempMovedScenePath), Is.EqualTo(originalGuid));
|
|
615
|
+
|
|
616
|
+
Assert.That(EditorBuildSettings.scenes, Has.Length.EqualTo(1));
|
|
617
|
+
Assert.That(EditorBuildSettings.scenes[0].path, Is.EqualTo(TempMovedScenePath));
|
|
618
|
+
|
|
619
|
+
EditorSceneManager.OpenScene(TempMovedScenePath, OpenSceneMode.Single);
|
|
620
|
+
Assert.That(GameObject.Find("MovedSceneMarker"), Is.Not.Null);
|
|
621
|
+
}
|
|
622
|
+
finally
|
|
623
|
+
{
|
|
624
|
+
EditorBuildSettings.scenes = originalScenes;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
480
628
|
[Test]
|
|
481
629
|
public void FileController_WritePatchRead_AndRejectsPathTraversal()
|
|
482
630
|
{
|
|
@@ -935,6 +1083,24 @@ namespace UCP.Bridge.Tests
|
|
|
935
1083
|
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
936
1084
|
}
|
|
937
1085
|
|
|
1086
|
+
private static void DeleteTempScene()
|
|
1087
|
+
{
|
|
1088
|
+
if (AssetDatabase.LoadMainAssetAtPath(TempScenePath) != null)
|
|
1089
|
+
{
|
|
1090
|
+
AssetDatabase.DeleteAsset(TempScenePath);
|
|
1091
|
+
AssetDatabase.SaveAssets();
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
private static void DeleteTempMovedFolder()
|
|
1096
|
+
{
|
|
1097
|
+
if (AssetDatabase.IsValidFolder(TempMovedFolderPath))
|
|
1098
|
+
{
|
|
1099
|
+
AssetDatabase.DeleteAsset(TempMovedFolderPath);
|
|
1100
|
+
AssetDatabase.SaveAssets();
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
938
1104
|
private static void DeleteTempProfilerExport()
|
|
939
1105
|
{
|
|
940
1106
|
var fullPath = ResolveProjectRelativePath(TempProfilerExportPath);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "com.ucp.bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"displayName": "Unity Control Protocol Bridge",
|
|
5
5
|
"description": "WebSocket bridge for programmatic Unity Editor control via CLI and AI agents.",
|
|
6
6
|
"unity": "2021.3",
|
|
@@ -23,5 +23,7 @@
|
|
|
23
23
|
"type": "tool",
|
|
24
24
|
"documentationUrl": "https://github.com/mflRevan/unity-control-protocol/blob/main/README.md",
|
|
25
25
|
"changelogUrl": "https://github.com/mflRevan/unity-control-protocol/blob/main/unity-package/com.ucp.bridge/CHANGELOG.md",
|
|
26
|
-
"dependencies": {
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"com.unity.test-framework": "1.1.31"
|
|
28
|
+
}
|
|
27
29
|
}
|