@mflrevan/ucp 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs +3 -0
- package/bridge/com.ucp.bridge/Editor/AssemblyInfo.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +45 -5
- package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +26 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +2 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +137 -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/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 +1 -35
- package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/ProfilerController.cs +80 -5
- 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 +26 -36
- package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs +82 -23
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +4 -4
- 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/Editor/Protocol/MiniJson.cs +409 -63
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +131 -13
- package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs +221 -0
- package/bridge/com.ucp.bridge/Tests/Editor/MiniJsonSerializerTests.cs.meta +2 -0
- 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.1` of the Unity Control Protocol CLI.
|
|
4
4
|
|
|
5
5
|
This package installs the `ucp` command, downloads the matching published binary for your platform during `postinstall`, and ships the matching Unity bridge payload inside the npm package.
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ namespace UCP.Bridge
|
|
|
26
26
|
private const int DefaultPort = 21342;
|
|
27
27
|
private const int MaxPort = 21352;
|
|
28
28
|
private const int MaxConnections = 4;
|
|
29
|
-
private const string ProtocolVersion = "0.
|
|
29
|
+
private const string ProtocolVersion = "0.6.1";
|
|
30
30
|
|
|
31
31
|
private static TcpListener s_listener;
|
|
32
32
|
private static CancellationTokenSource s_cts;
|
|
@@ -36,6 +36,13 @@ namespace UCP.Bridge
|
|
|
36
36
|
private static string s_token;
|
|
37
37
|
private static bool s_running;
|
|
38
38
|
|
|
39
|
+
// Handshake payload, captured once on the main thread at startup. `Application.dataPath`
|
|
40
|
+
// and friends are main-thread-only, which is the sole reason the handshake used to be
|
|
41
|
+
// queued behind `EditorApplication.update` -- see the socket-thread fast path in Dispatch.
|
|
42
|
+
private static string s_unityVersion = string.Empty;
|
|
43
|
+
private static string s_projectName = string.Empty;
|
|
44
|
+
private static string s_projectPath = string.Empty;
|
|
45
|
+
|
|
39
46
|
// Main-thread action queue
|
|
40
47
|
private static readonly ConcurrentQueue<Action> s_mainThreadQueue = new();
|
|
41
48
|
|
|
@@ -71,6 +78,7 @@ namespace UCP.Bridge
|
|
|
71
78
|
|
|
72
79
|
try
|
|
73
80
|
{
|
|
81
|
+
CaptureEditorIdentity();
|
|
74
82
|
RegisterHandlers();
|
|
75
83
|
LogsController.SeedHistoryFromConsole();
|
|
76
84
|
|
|
@@ -88,18 +96,31 @@ namespace UCP.Bridge
|
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
|
|
99
|
+
/// <summary>
|
|
100
|
+
/// Read the editor/project identity once, on the main thread, so the handshake handler is
|
|
101
|
+
/// pure and can run anywhere.
|
|
102
|
+
/// </summary>
|
|
103
|
+
private static void CaptureEditorIdentity()
|
|
104
|
+
{
|
|
105
|
+
s_unityVersion = Application.unityVersion;
|
|
106
|
+
s_projectName = Application.productName;
|
|
107
|
+
s_projectPath = Path.GetDirectoryName(Application.dataPath);
|
|
108
|
+
}
|
|
109
|
+
|
|
91
110
|
private static void RegisterHandlers()
|
|
92
111
|
{
|
|
93
112
|
// Handshake
|
|
94
113
|
s_router.Register("handshake", (paramsJson) =>
|
|
95
114
|
{
|
|
115
|
+
// Touches no Unity API -- see CaptureEditorIdentity. Keep it that way: the
|
|
116
|
+
// socket-thread fast path in Dispatch depends on it.
|
|
96
117
|
return new
|
|
97
118
|
{
|
|
98
119
|
serverVersion = ProtocolVersion,
|
|
99
120
|
protocolVersion = ProtocolVersion,
|
|
100
|
-
unityVersion =
|
|
101
|
-
projectName =
|
|
102
|
-
projectPath =
|
|
121
|
+
unityVersion = s_unityVersion,
|
|
122
|
+
projectName = s_projectName,
|
|
123
|
+
projectPath = s_projectPath
|
|
103
124
|
};
|
|
104
125
|
});
|
|
105
126
|
|
|
@@ -145,6 +166,15 @@ namespace UCP.Bridge
|
|
|
145
166
|
// Hierarchy Operations
|
|
146
167
|
HierarchyController.Register(s_router);
|
|
147
168
|
|
|
169
|
+
// Transform authoring (move/rotate/scale/look-at, bulk read)
|
|
170
|
+
TransformController.Register(s_router);
|
|
171
|
+
|
|
172
|
+
// Spatial queries (raycast/overlap/bounds/ground/nearest)
|
|
173
|
+
SpatialController.Register(s_router);
|
|
174
|
+
|
|
175
|
+
// Composed visual perception (capture/isolate/orbit)
|
|
176
|
+
ViewController.Register(s_router);
|
|
177
|
+
|
|
148
178
|
// Asset Management
|
|
149
179
|
AssetController.Register(s_router);
|
|
150
180
|
ImporterController.Register(s_router);
|
|
@@ -384,12 +414,22 @@ namespace UCP.Bridge
|
|
|
384
414
|
lock (s_clientLock) { s_logSubscribers.Remove(ws); }
|
|
385
415
|
}
|
|
386
416
|
|
|
387
|
-
// Dispatch on main thread
|
|
388
417
|
var capturedId = id;
|
|
389
418
|
var capturedMethod = method;
|
|
390
419
|
var capturedParams = paramsJson;
|
|
391
420
|
var capturedWs = ws;
|
|
392
421
|
|
|
422
|
+
// Answer the handshake straight off the socket thread. Everything it returns is
|
|
423
|
+
// cached, so queueing it only bought a wait for the next `EditorApplication.update`
|
|
424
|
+
// tick -- roughly 90 ms on an idle editor, paid by *every* CLI command before its
|
|
425
|
+
// real request could even be dispatched.
|
|
426
|
+
if (capturedMethod == "handshake")
|
|
427
|
+
{
|
|
428
|
+
SendResponse(capturedWs, s_router.Dispatch(capturedMethod, capturedId, capturedParams));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Dispatch on main thread
|
|
393
433
|
s_mainThreadQueue.Enqueue(() =>
|
|
394
434
|
{
|
|
395
435
|
var response = s_router.Dispatch(capturedMethod, capturedId, capturedParams);
|
|
@@ -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
|
|
@@ -125,7 +125,7 @@ namespace UCP.Bridge
|
|
|
125
125
|
["name"] = asset.name,
|
|
126
126
|
["type"] = asset.GetType().Name,
|
|
127
127
|
["fullType"] = asset.GetType().FullName,
|
|
128
|
-
["instanceId"] = asset.
|
|
128
|
+
["instanceId"] = asset.GetId(),
|
|
129
129
|
["guid"] = AssetDatabase.AssetPathToGUID(assetPath)
|
|
130
130
|
};
|
|
131
131
|
|
|
@@ -496,7 +496,7 @@ namespace UCP.Bridge
|
|
|
496
496
|
["status"] = "ok",
|
|
497
497
|
["path"] = assetPath,
|
|
498
498
|
["type"] = soType.Name,
|
|
499
|
-
["instanceId"] = instance.
|
|
499
|
+
["instanceId"] = instance.GetId()
|
|
500
500
|
};
|
|
501
501
|
}
|
|
502
502
|
|
|
@@ -9,19 +9,86 @@ namespace UCP.Bridge
|
|
|
9
9
|
{
|
|
10
10
|
public static class CompilationController
|
|
11
11
|
{
|
|
12
|
+
// SessionState survives domain reloads within one editor session. That is exactly the
|
|
13
|
+
// window we need: a compile that SUCCEEDS reloads the domain, a compile that FAILS keeps
|
|
14
|
+
// the old domain (no reload), and in both cases the captured diagnostics must still be
|
|
15
|
+
// readable by the CLI after the editor settles. CompileDiagnosticsTracker writes here;
|
|
16
|
+
// HandleDiagnostics reads it back.
|
|
17
|
+
internal const string DiagStateKey = "ucp.compile.diagnostics.state";
|
|
18
|
+
internal const string DiagRequestKey = "ucp.compile.diagnostics.requestId";
|
|
19
|
+
private const int MaxDiagnosticMessages = 200;
|
|
20
|
+
|
|
12
21
|
public static void Register(CommandRouter router)
|
|
13
22
|
{
|
|
14
23
|
router.Register("compile", HandleCompile);
|
|
24
|
+
router.Register("compile/diagnostics", HandleDiagnostics);
|
|
15
25
|
router.Register("refresh-assets", HandleRefresh);
|
|
16
26
|
router.Register("script/doctor", HandleScriptDoctor);
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
private static object HandleCompile(string paramsJson)
|
|
20
30
|
{
|
|
31
|
+
// Stamp a fresh request id and reset the diagnostics buffer so the CLI can tell THIS
|
|
32
|
+
// compile's result apart from a stale one. CompileDiagnosticsTracker fills in the
|
|
33
|
+
// per-assembly CompilerMessages as compilation progresses and finishes.
|
|
34
|
+
var requestId = SessionState.GetInt(DiagRequestKey, 0) + 1;
|
|
35
|
+
SessionState.SetInt(DiagRequestKey, requestId);
|
|
36
|
+
SessionState.SetString(DiagStateKey, MiniJson.Serialize(new Dictionary<string, object>
|
|
37
|
+
{
|
|
38
|
+
["status"] = "requested",
|
|
39
|
+
["requestId"] = requestId,
|
|
40
|
+
["errorCount"] = 0,
|
|
41
|
+
["warningCount"] = 0,
|
|
42
|
+
["messages"] = new List<object>()
|
|
43
|
+
}));
|
|
44
|
+
|
|
21
45
|
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
|
22
46
|
CompilationPipeline.RequestScriptCompilation();
|
|
23
47
|
TrySyncSolution();
|
|
24
|
-
return new
|
|
48
|
+
return new Dictionary<string, object>
|
|
49
|
+
{
|
|
50
|
+
["status"] = "ok",
|
|
51
|
+
["message"] = "Asset database refreshed and compilation requested",
|
|
52
|
+
["requestId"] = requestId
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private static object HandleDiagnostics(string paramsJson)
|
|
57
|
+
{
|
|
58
|
+
var raw = SessionState.GetString(DiagStateKey, string.Empty);
|
|
59
|
+
if (string.IsNullOrEmpty(raw)
|
|
60
|
+
|| !(MiniJson.Deserialize(raw) is Dictionary<string, object> state))
|
|
61
|
+
{
|
|
62
|
+
return new Dictionary<string, object>
|
|
63
|
+
{
|
|
64
|
+
["status"] = "idle",
|
|
65
|
+
["requestId"] = SessionState.GetInt(DiagRequestKey, 0),
|
|
66
|
+
["errorCount"] = 0,
|
|
67
|
+
["warningCount"] = 0,
|
|
68
|
+
["messages"] = new List<object>(),
|
|
69
|
+
["compiling"] = EditorApplication.isCompiling
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Surface live compile state too: a "completed" buffer while isCompiling is true means
|
|
74
|
+
// another compilation has already started after the one the CLI asked about.
|
|
75
|
+
state["compiling"] = EditorApplication.isCompiling;
|
|
76
|
+
return state;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
internal static void WriteDiagnosticsState(string status, int errorCount, int warningCount, List<object> messages)
|
|
80
|
+
{
|
|
81
|
+
var truncated = messages.Count > MaxDiagnosticMessages;
|
|
82
|
+
var trimmed = truncated ? messages.GetRange(0, MaxDiagnosticMessages) : messages;
|
|
83
|
+
SessionState.SetString(DiagStateKey, MiniJson.Serialize(new Dictionary<string, object>
|
|
84
|
+
{
|
|
85
|
+
["status"] = status,
|
|
86
|
+
["requestId"] = SessionState.GetInt(DiagRequestKey, 0),
|
|
87
|
+
["errorCount"] = errorCount,
|
|
88
|
+
["warningCount"] = warningCount,
|
|
89
|
+
["truncated"] = truncated,
|
|
90
|
+
["messages"] = new List<object>(trimmed)
|
|
91
|
+
}));
|
|
25
92
|
}
|
|
26
93
|
|
|
27
94
|
private static object HandleRefresh(string paramsJson)
|
|
@@ -110,4 +177,73 @@ namespace UCP.Bridge
|
|
|
110
177
|
}
|
|
111
178
|
}
|
|
112
179
|
}
|
|
180
|
+
|
|
181
|
+
/// <summary>
|
|
182
|
+
/// Captures per-assembly compiler messages so `compile` can report whether the build actually
|
|
183
|
+
/// succeeded instead of always claiming success. Subscribes once per domain load; accumulates
|
|
184
|
+
/// messages for the in-flight compilation in static fields (safe because a domain reload only
|
|
185
|
+
/// happens AFTER compilationFinished) and flushes them to SessionState, which persists across
|
|
186
|
+
/// the reload for the CLI to read once the editor settles.
|
|
187
|
+
/// </summary>
|
|
188
|
+
[InitializeOnLoad]
|
|
189
|
+
internal static class CompileDiagnosticsTracker
|
|
190
|
+
{
|
|
191
|
+
private static readonly List<object> s_messages = new List<object>();
|
|
192
|
+
private static int s_errorCount;
|
|
193
|
+
private static int s_warningCount;
|
|
194
|
+
|
|
195
|
+
static CompileDiagnosticsTracker()
|
|
196
|
+
{
|
|
197
|
+
CompilationPipeline.compilationStarted += OnCompilationStarted;
|
|
198
|
+
CompilationPipeline.assemblyCompilationFinished += OnAssemblyCompilationFinished;
|
|
199
|
+
CompilationPipeline.compilationFinished += OnCompilationFinished;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private static void OnCompilationStarted(object context)
|
|
203
|
+
{
|
|
204
|
+
s_messages.Clear();
|
|
205
|
+
s_errorCount = 0;
|
|
206
|
+
s_warningCount = 0;
|
|
207
|
+
CompilationController.WriteDiagnosticsState("compiling", 0, 0, s_messages);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private static void OnAssemblyCompilationFinished(string assemblyPath, CompilerMessage[] messages)
|
|
211
|
+
{
|
|
212
|
+
if (messages == null) return;
|
|
213
|
+
var assembly = Path.GetFileNameWithoutExtension(assemblyPath);
|
|
214
|
+
foreach (var message in messages)
|
|
215
|
+
{
|
|
216
|
+
string type;
|
|
217
|
+
switch (message.type)
|
|
218
|
+
{
|
|
219
|
+
case CompilerMessageType.Error:
|
|
220
|
+
type = "error";
|
|
221
|
+
s_errorCount++;
|
|
222
|
+
break;
|
|
223
|
+
case CompilerMessageType.Warning:
|
|
224
|
+
type = "warning";
|
|
225
|
+
s_warningCount++;
|
|
226
|
+
break;
|
|
227
|
+
default:
|
|
228
|
+
type = "info";
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
s_messages.Add(new Dictionary<string, object>
|
|
233
|
+
{
|
|
234
|
+
["assembly"] = assembly,
|
|
235
|
+
["type"] = type,
|
|
236
|
+
["message"] = message.message ?? string.Empty,
|
|
237
|
+
["file"] = message.file ?? string.Empty,
|
|
238
|
+
["line"] = message.line,
|
|
239
|
+
["column"] = message.column
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private static void OnCompilationFinished(object context)
|
|
245
|
+
{
|
|
246
|
+
CompilationController.WriteDiagnosticsState("completed", s_errorCount, s_warningCount, s_messages);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
113
249
|
}
|
|
@@ -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);
|
|
@@ -50,7 +50,7 @@ namespace UCP.Bridge
|
|
|
50
50
|
["path"] = path,
|
|
51
51
|
["name"] = material.name,
|
|
52
52
|
["shader"] = shader.name,
|
|
53
|
-
["instanceId"] = material.
|
|
53
|
+
["instanceId"] = material.GetId()
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -278,7 +278,7 @@ namespace UCP.Bridge
|
|
|
278
278
|
{
|
|
279
279
|
["name"] = tex.name,
|
|
280
280
|
["path"] = texPath,
|
|
281
|
-
["instanceId"] = tex.
|
|
281
|
+
["instanceId"] = tex.GetId()
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
284
|
return null;
|