@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.
- package/README.md +1 -1
- package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs +3 -0
- package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +16 -3
- package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +26 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +172 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +88 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs +60 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorModalGuard.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +56 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +325 -13
- package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +207 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +14 -35
- package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/ReferenceController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +6 -6
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +2 -34
- package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs +151 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +304 -9
- package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs +322 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SpatialController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +249 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +415 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +135 -7
- package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +252 -0
- package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs.meta +2 -0
- package/bridge/com.ucp.bridge/package.json +1 -1
- 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.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
|
|
|
@@ -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.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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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);
|