@mflrevan/ucp 0.5.1 → 0.6.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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs +3 -0
  3. package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs.meta +2 -0
  4. package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +16 -3
  5. package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +26 -0
  6. package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +172 -2
  7. package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +88 -1
  8. package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs +60 -0
  9. package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs.meta +2 -0
  10. package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +56 -5
  11. package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +325 -13
  12. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
  13. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +207 -0
  14. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs.meta +2 -0
  15. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +1 -1
  16. package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +14 -35
  17. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +3 -3
  18. package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +1 -1
  19. package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs +1 -1
  20. package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +6 -6
  21. package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +2 -34
  22. package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs +151 -0
  23. package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs.meta +2 -0
  24. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +304 -9
  25. package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs +322 -0
  26. package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs.meta +2 -0
  27. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +249 -0
  28. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs.meta +2 -0
  29. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +415 -0
  30. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs.meta +2 -0
  31. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +135 -7
  32. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +252 -0
  33. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs.meta +2 -0
  34. package/bridge/com.ucp.bridge/package.json +1 -1
  35. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @mflrevan/ucp
2
2
 
3
- Version `0.5.1` of the Unity Control Protocol CLI.
3
+ Version `0.6.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
 
@@ -0,0 +1,3 @@
1
+ using System.Runtime.CompilerServices;
2
+
3
+ [assembly: InternalsVisibleTo("UCP.Bridge.Editor.Tests")]
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: a1b2c3d4e5f60718293a4b5c6d7e8f90
@@ -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.1";
29
+ private const string ProtocolVersion = "0.6.0";
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.logMessageReceived += OnLogMessage;
80
+ Application.logMessageReceivedThreaded += OnLogMessage;
80
81
 
81
82
  s_token = Guid.NewGuid().ToString("N").Substring(0, 16);
82
83
  StartServer();
@@ -144,6 +145,15 @@ namespace UCP.Bridge
144
145
  // Hierarchy Operations
145
146
  HierarchyController.Register(s_router);
146
147
 
148
+ // Transform authoring (move/rotate/scale/look-at, bulk read)
149
+ TransformController.Register(s_router);
150
+
151
+ // Spatial queries (raycast/overlap/bounds/ground/nearest)
152
+ SpatialController.Register(s_router);
153
+
154
+ // Composed visual perception (capture/isolate/orbit)
155
+ ViewController.Register(s_router);
156
+
147
157
  // Asset Management
148
158
  AssetController.Register(s_router);
149
159
  ImporterController.Register(s_router);
@@ -165,6 +175,9 @@ namespace UCP.Bridge
165
175
 
166
176
  // Reference search (bridge fallback)
167
177
  ReferenceController.Register(s_router);
178
+
179
+ // Shader diagnostics
180
+ ShaderController.Register(s_router);
168
181
  }
169
182
 
170
183
  private static void StartServer()
@@ -555,7 +568,7 @@ namespace UCP.Bridge
555
568
  CleanLockFile();
556
569
 
557
570
  EditorApplication.update -= PumpMainThread;
558
- Application.logMessageReceived -= OnLogMessage;
571
+ Application.logMessageReceivedThreaded -= OnLogMessage;
559
572
  }
560
573
 
561
574
  private static Dictionary<string, object> ResponseToDict(JsonRpcResponse r)
@@ -1,13 +1,39 @@
1
1
  using UnityEditor;
2
2
  using UnityEngine;
3
+ using UnityEngine.SceneManagement;
3
4
 
4
5
  namespace UCP.Bridge
5
6
  {
6
7
  internal static class UnityObjectCompat
7
8
  {
9
+ public static int GetId(this Object obj)
10
+ {
11
+ #if UNITY_6000_5_OR_NEWER
12
+ return unchecked((int)EntityId.ToULong(obj.GetEntityId()));
13
+ #else
14
+ return obj.GetInstanceID();
15
+ #endif
16
+ }
17
+
18
+ public static long GetSceneHandle(Scene scene)
19
+ {
20
+ #if UNITY_6000_5_OR_NEWER
21
+ return unchecked((long)scene.handle.GetRawData());
22
+ #else
23
+ // On 6000.0–6000.4, Scene.handle is a SceneHandle with implicit int and
24
+ // uint operators; widening straight to long is ambiguous (CS0457). Pin the
25
+ // int conversion explicitly — handles are 32-bit on these versions.
26
+ return (int)scene.handle;
27
+ #endif
28
+ }
29
+
8
30
  public static Object ResolveByInstanceId(int instanceId)
9
31
  {
32
+ #if UNITY_6000_5_OR_NEWER
33
+ return EditorUtility.EntityIdToObject(EntityId.FromULong(unchecked((ulong)instanceId)));
34
+ #else
10
35
  return EditorUtility.InstanceIDToObject(instanceId);
36
+ #endif
11
37
  }
12
38
 
13
39
  public static T ResolveByInstanceId<T>(int instanceId) where T : Object
@@ -5,6 +5,7 @@ using System.Reflection;
5
5
  using System.Text.RegularExpressions;
6
6
  using UnityEditor;
7
7
  using UnityEngine;
8
+ using UnityEngine.Rendering;
8
9
 
9
10
  namespace UCP.Bridge
10
11
  {
@@ -14,6 +15,7 @@ namespace UCP.Bridge
14
15
  {
15
16
  router.Register("asset/search", HandleSearch);
16
17
  router.Register("asset/info", HandleInfo);
18
+ router.Register("asset/inspect", HandleInspect);
17
19
  router.Register("asset/read", HandleReadScriptableObject);
18
20
  router.Register("asset/write", HandleWriteScriptableObject);
19
21
  router.Register("asset/write-batch", HandleWriteScriptableObjectBatch);
@@ -123,7 +125,7 @@ namespace UCP.Bridge
123
125
  ["name"] = asset.name,
124
126
  ["type"] = asset.GetType().Name,
125
127
  ["fullType"] = asset.GetType().FullName,
126
- ["instanceId"] = asset.GetInstanceID(),
128
+ ["instanceId"] = asset.GetId(),
127
129
  ["guid"] = AssetDatabase.AssetPathToGUID(assetPath)
128
130
  };
129
131
 
@@ -152,6 +154,47 @@ namespace UCP.Bridge
152
154
  return info;
153
155
  }
154
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
+
155
198
  private static object HandleReadScriptableObject(string paramsJson)
156
199
  {
157
200
  var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
@@ -211,6 +254,133 @@ namespace UCP.Bridge
211
254
  };
212
255
  }
213
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
+
214
384
  private static object HandleWriteScriptableObject(string paramsJson)
215
385
  {
216
386
  var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
@@ -326,7 +496,7 @@ namespace UCP.Bridge
326
496
  ["status"] = "ok",
327
497
  ["path"] = assetPath,
328
498
  ["type"] = soType.Name,
329
- ["instanceId"] = instance.GetInstanceID()
499
+ ["instanceId"] = instance.GetId()
330
500
  };
331
501
  }
332
502
 
@@ -1,5 +1,9 @@
1
1
  using UnityEditor;
2
2
  using UnityEditor.Compilation;
3
+ using System;
4
+ using System.Collections.Generic;
5
+ using System.IO;
6
+ using System.Text.RegularExpressions;
3
7
 
4
8
  namespace UCP.Bridge
5
9
  {
@@ -9,12 +13,15 @@ namespace UCP.Bridge
9
13
  {
10
14
  router.Register("compile", HandleCompile);
11
15
  router.Register("refresh-assets", HandleRefresh);
16
+ router.Register("script/doctor", HandleScriptDoctor);
12
17
  }
13
18
 
14
19
  private static object HandleCompile(string paramsJson)
15
20
  {
21
+ AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
16
22
  CompilationPipeline.RequestScriptCompilation();
17
- return new { status = "ok", message = "Compilation requested" };
23
+ TrySyncSolution();
24
+ return new { status = "ok", message = "Asset database refreshed and compilation requested" };
18
25
  }
19
26
 
20
27
  private static object HandleRefresh(string paramsJson)
@@ -22,5 +29,85 @@ namespace UCP.Bridge
22
29
  AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
23
30
  return new { status = "ok", message = "Asset database refreshed" };
24
31
  }
32
+
33
+ private static object HandleScriptDoctor(string paramsJson)
34
+ {
35
+ var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
36
+ var fix = p != null && p.TryGetValue("fix", out var fixObj) && fixObj != null && Convert.ToBoolean(fixObj);
37
+ var projectRoot = Directory.GetParent(UnityEngine.Application.dataPath).FullName;
38
+ var projects = new List<object>();
39
+ var staleProjectCount = 0;
40
+ var missingFileCount = 0;
41
+ var deletedProjectCount = 0;
42
+
43
+ foreach (var csproj in Directory.GetFiles(projectRoot, "*.csproj", SearchOption.TopDirectoryOnly))
44
+ {
45
+ var missing = FindMissingCompileItems(projectRoot, csproj);
46
+ if (missing.Count > 0)
47
+ {
48
+ staleProjectCount++;
49
+ missingFileCount += missing.Count;
50
+ if (fix)
51
+ {
52
+ File.Delete(csproj);
53
+ deletedProjectCount++;
54
+ }
55
+ }
56
+
57
+ projects.Add(new Dictionary<string, object>
58
+ {
59
+ ["path"] = csproj,
60
+ ["missingCompileItems"] = missing.ConvertAll<object>(item => item),
61
+ ["stale"] = missing.Count > 0
62
+ });
63
+ }
64
+
65
+ if (fix)
66
+ {
67
+ AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
68
+ TrySyncSolution();
69
+ }
70
+
71
+ return new Dictionary<string, object>
72
+ {
73
+ ["status"] = "ok",
74
+ ["projectRoot"] = projectRoot,
75
+ ["projectCount"] = projects.Count,
76
+ ["staleProjectCount"] = staleProjectCount,
77
+ ["missingFileCount"] = missingFileCount,
78
+ ["deletedProjectCount"] = deletedProjectCount,
79
+ ["fixed"] = fix,
80
+ ["projects"] = projects
81
+ };
82
+ }
83
+
84
+ private static List<string> FindMissingCompileItems(string projectRoot, string csproj)
85
+ {
86
+ var missing = new List<string>();
87
+ var content = File.ReadAllText(csproj);
88
+ foreach (Match match in Regex.Matches(content, "<Compile Include=\"([^\"]+\\.cs)\""))
89
+ {
90
+ var include = match.Groups[1].Value.Replace('\\', Path.DirectorySeparatorChar);
91
+ var fullPath = Path.GetFullPath(Path.Combine(projectRoot, include));
92
+ if (!File.Exists(fullPath))
93
+ missing.Add(include.Replace('\\', '/'));
94
+ }
95
+
96
+ return missing;
97
+ }
98
+
99
+ private static void TrySyncSolution()
100
+ {
101
+ try
102
+ {
103
+ var syncVs = typeof(Editor).Assembly.GetType("UnityEditor.SyncVS");
104
+ var method = syncVs?.GetMethod("SyncSolution", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic, null, Type.EmptyTypes, null);
105
+ method?.Invoke(null, null);
106
+ }
107
+ catch
108
+ {
109
+ // Best-effort only; Unity may regenerate solution files asynchronously.
110
+ }
111
+ }
25
112
  }
26
113
  }
@@ -0,0 +1,60 @@
1
+ using UnityEditor;
2
+ using UnityEditor.SceneManagement;
3
+ using UnityEngine.SceneManagement;
4
+
5
+ namespace UCP.Bridge
6
+ {
7
+ /// <summary>
8
+ /// Centralizes editor operations that must stay non-blocking when the bridge drives
9
+ /// the editor headlessly.
10
+ ///
11
+ /// Bridge commands run on the main thread via EditorApplication.update. Any modal
12
+ /// dialog (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo,
13
+ /// EditorUtility.DisplayDialog, file/folder panels, ...) blocks that loop with no user
14
+ /// present to dismiss it, hanging every queued command and timing out the CLI. Bridge
15
+ /// code must therefore never call a modal API. Scene saving is the most common trap, so
16
+ /// it lives here behind a single audited, modal-free implementation shared by every
17
+ /// controller instead of being copy-pasted per controller.
18
+ /// </summary>
19
+ internal static class EditorModalGuard
20
+ {
21
+ /// <summary>
22
+ /// Saves every loaded dirty scene without prompting. Dirty untitled scenes have no
23
+ /// path to save to; they are discarded (replaced with a fresh empty scene) when
24
+ /// <paramref name="discardUntitled"/> is true, otherwise an exception is thrown so
25
+ /// the caller surfaces a clear error instead of silently losing work or blocking on
26
+ /// a save dialog.
27
+ /// </summary>
28
+ public static void SaveOpenDirtyScenes(bool saveDirtyScenes, bool discardUntitled)
29
+ {
30
+ if (!saveDirtyScenes)
31
+ return;
32
+
33
+ var requiresUntitledDiscard = false;
34
+
35
+ for (var index = 0; index < SceneManager.sceneCount; index++)
36
+ {
37
+ var scene = SceneManager.GetSceneAt(index);
38
+ if (!scene.isLoaded || !scene.isDirty)
39
+ continue;
40
+
41
+ if (string.IsNullOrEmpty(scene.path))
42
+ {
43
+ if (!discardUntitled)
44
+ throw new System.InvalidOperationException("Dirty untitled scene cannot be auto-saved. Retry with discardUntitled=true.");
45
+
46
+ requiresUntitledDiscard = true;
47
+ continue;
48
+ }
49
+
50
+ if (!EditorSceneManager.SaveScene(scene))
51
+ throw new System.InvalidOperationException($"Failed to auto-save dirty scene: {scene.path}");
52
+
53
+ SceneChangeTracker.ClearScene(scene);
54
+ }
55
+
56
+ if (requiresUntitledDiscard)
57
+ EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6
@@ -26,7 +26,19 @@ namespace UCP.Bridge
26
26
  if (p != null && p.TryGetValue("name", out var nameObj) && nameObj != null)
27
27
  name = nameObj.ToString();
28
28
 
29
- var go = new GameObject(name);
29
+ // Optional primitive: build a Cube/Sphere/etc. with mesh + collider in one step.
30
+ // Without this an agent has to hand-assemble MeshFilter + MeshRenderer and somehow
31
+ // reference the built-in mesh, which is not practical over the CLI.
32
+ GameObject go;
33
+ if (p != null && p.TryGetValue("primitive", out var primObj) && primObj != null)
34
+ {
35
+ go = GameObject.CreatePrimitive(ParsePrimitive(primObj.ToString()));
36
+ go.name = name;
37
+ }
38
+ else
39
+ {
40
+ go = new GameObject(name);
41
+ }
30
42
  Undo.RegisterCreatedObjectUndo(go, "UCP Create GameObject");
31
43
 
32
44
  // Optional parent
@@ -61,7 +73,7 @@ namespace UCP.Bridge
61
73
  return new Dictionary<string, object>
62
74
  {
63
75
  ["status"] = "ok",
64
- ["instanceId"] = go.GetInstanceID(),
76
+ ["instanceId"] = go.GetId(),
65
77
  ["name"] = go.name
66
78
  };
67
79
  }
@@ -140,7 +152,12 @@ namespace UCP.Bridge
140
152
  string prefabPath = prefabObj.ToString();
141
153
  var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
142
154
  if (prefab == null)
143
- throw new ArgumentException($"Prefab not found: {prefabPath}");
155
+ {
156
+ var hint = LooksLikePrimitive(prefabPath)
157
+ ? " To create a built-in primitive, use object create with a primitive instead (e.g. `ucp object create MyCube --primitive Cube`); instantiate is only for prefab assets under Assets/."
158
+ : string.Empty;
159
+ throw new ArgumentException($"Prefab not found: {prefabPath}.{hint}");
160
+ }
144
161
  source = prefab;
145
162
  }
146
163
  // Instantiate from existing scene object (clone)
@@ -192,7 +209,7 @@ namespace UCP.Bridge
192
209
  return new Dictionary<string, object>
193
210
  {
194
211
  ["status"] = "ok",
195
- ["instanceId"] = instance.GetInstanceID(),
212
+ ["instanceId"] = instance.GetId(),
196
213
  ["name"] = instance.name
197
214
  };
198
215
  }
@@ -267,6 +284,40 @@ namespace UCP.Bridge
267
284
  };
268
285
  }
269
286
 
287
+ private static bool LooksLikePrimitive(string path)
288
+ {
289
+ if (string.IsNullOrEmpty(path)) return false;
290
+ var token = path.Replace("PrimitiveType.", "").Trim();
291
+ switch (token.ToLowerInvariant())
292
+ {
293
+ case "cube":
294
+ case "sphere":
295
+ case "capsule":
296
+ case "cylinder":
297
+ case "plane":
298
+ case "quad":
299
+ return true;
300
+ default:
301
+ return path.IndexOf("PrimitiveType", System.StringComparison.OrdinalIgnoreCase) >= 0;
302
+ }
303
+ }
304
+
305
+ private static PrimitiveType ParsePrimitive(string value)
306
+ {
307
+ switch (value.Trim().ToLowerInvariant())
308
+ {
309
+ case "cube": return PrimitiveType.Cube;
310
+ case "sphere": return PrimitiveType.Sphere;
311
+ case "capsule": return PrimitiveType.Capsule;
312
+ case "cylinder": return PrimitiveType.Cylinder;
313
+ case "plane": return PrimitiveType.Plane;
314
+ case "quad": return PrimitiveType.Quad;
315
+ default:
316
+ throw new ArgumentException(
317
+ $"Unknown primitive '{value}'. Use one of: Cube, Sphere, Capsule, Cylinder, Plane, Quad.");
318
+ }
319
+ }
320
+
270
321
  private static Type ResolveComponentType(string typeName)
271
322
  {
272
323
  // Try exact match first
@@ -314,7 +365,7 @@ namespace UCP.Bridge
314
365
 
315
366
  private static GameObject FindInHierarchy(GameObject go, int instanceId)
316
367
  {
317
- if (go.GetInstanceID() == instanceId) return go;
368
+ if (go.GetId() == instanceId) return go;
318
369
  for (int i = 0; i < go.transform.childCount; i++)
319
370
  {
320
371
  var found = FindInHierarchy(go.transform.GetChild(i).gameObject, instanceId);