@mflrevan/ucp 0.2.3 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @mflrevan/ucp
2
2
 
3
- Version `0.2.3` of the Unity Control Protocol CLI.
3
+ Version `0.3.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
 
@@ -42,12 +42,14 @@ ucp build targets
42
42
  ucp install
43
43
  ```
44
44
 
45
+ Default `ucp install` writes a tracked git dependency to `Packages/manifest.json` pinned to the CLI version. It does not write a local `file:` dependency unless you explicitly choose a local embedded mode.
46
+
45
47
  Or add this to `Packages/manifest.json`:
46
48
 
47
49
  ```json
48
50
  {
49
51
  "dependencies": {
50
- "com.ucp.bridge": "https://github.com/mflRevan/unity-control-protocol.git?path=unity-package/com.ucp.bridge#v0.2.3"
52
+ "com.ucp.bridge": "https://github.com/mflRevan/unity-control-protocol.git?path=unity-package/com.ucp.bridge#v0.3.0"
51
53
  }
52
54
  }
53
55
  ```
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-03-13
4
+
5
+ ### Added
6
+
7
+ - Dirty-scene automation controls in bridge scene/play handlers so unattended CLI workflows can avoid save-confirmation modal interruptions.
8
+
9
+ ### Changed
10
+
11
+ - Scene load and play entry now auto-handle dirty scenes by default for non-interactive automation flows.
12
+
13
+ ### Fixed
14
+
15
+ - Fixed edit-mode test execution when called during Play Mode by deferring run start until Play Mode exits.
16
+ - Fixed workflow interruptions caused by Unity save-scene prompts during scene transitions and play mode entry.
17
+
3
18
  ## [0.2.3] - 2026-03-12
4
19
 
5
20
  ### Fixed
@@ -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.2.3";
29
+ private const string ProtocolVersion = "0.3.0";
30
30
 
31
31
  private static TcpListener s_listener;
32
32
  private static CancellationTokenSource s_cts;
@@ -1,4 +1,7 @@
1
1
  using UnityEditor;
2
+ using UnityEditor.SceneManagement;
3
+ using UnityEngine.SceneManagement;
4
+ using System.Collections.Generic;
2
5
 
3
6
  namespace UCP.Bridge
4
7
  {
@@ -16,10 +19,53 @@ namespace UCP.Bridge
16
19
  if (EditorApplication.isPlaying)
17
20
  return new { status = "already_playing" };
18
21
 
22
+ var saveDirtyScenes = GetBoolParam(paramsJson, "saveDirtyScenes", true);
23
+ var discardUntitled = GetBoolParam(paramsJson, "discardUntitled", true);
24
+ SaveDirtyScenesIfRequested(saveDirtyScenes, discardUntitled);
25
+
19
26
  EditorApplication.isPlaying = true;
20
27
  return new { status = "ok" };
21
28
  }
22
29
 
30
+ private static bool GetBoolParam(string paramsJson, string key, bool defaultValue)
31
+ {
32
+ var parameters = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
33
+ if (parameters != null && parameters.TryGetValue(key, out var valueObj) && valueObj is bool value)
34
+ return value;
35
+
36
+ return defaultValue;
37
+ }
38
+
39
+ private static void SaveDirtyScenesIfRequested(bool saveDirtyScenes, bool discardUntitled)
40
+ {
41
+ if (!saveDirtyScenes)
42
+ return;
43
+
44
+ var requiresUntitledDiscard = false;
45
+
46
+ for (var index = 0; index < SceneManager.sceneCount; index++)
47
+ {
48
+ var scene = SceneManager.GetSceneAt(index);
49
+ if (!scene.isLoaded || !scene.isDirty)
50
+ continue;
51
+
52
+ if (string.IsNullOrEmpty(scene.path))
53
+ {
54
+ if (!discardUntitled)
55
+ throw new System.InvalidOperationException("Dirty untitled scene cannot be auto-saved. Retry with discardUntitled=true.");
56
+
57
+ requiresUntitledDiscard = true;
58
+ continue;
59
+ }
60
+
61
+ if (!EditorSceneManager.SaveScene(scene))
62
+ throw new System.InvalidOperationException($"Failed to auto-save dirty scene: {scene.path}");
63
+ }
64
+
65
+ if (requiresUntitledDiscard)
66
+ EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
67
+ }
68
+
23
69
  private static object HandleStop(string paramsJson)
24
70
  {
25
71
  if (!EditorApplication.isPlaying)
@@ -40,6 +40,8 @@ namespace UCP.Bridge
40
40
  throw new System.ArgumentException("Missing 'path' parameter");
41
41
 
42
42
  var path = pathObj.ToString();
43
+ var saveDirtyScenes = GetBoolParam(p, "saveDirtyScenes", true);
44
+ var discardUntitled = GetBoolParam(p, "discardUntitled", true);
43
45
 
44
46
  if (EditorApplication.isPlaying)
45
47
  {
@@ -47,12 +49,51 @@ namespace UCP.Bridge
47
49
  }
48
50
  else
49
51
  {
50
- EditorSceneManager.OpenScene(path);
52
+ SaveDirtyScenesIfRequested(saveDirtyScenes, discardUntitled);
53
+ EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
51
54
  }
52
55
 
53
56
  return new { status = "ok", loaded = path };
54
57
  }
55
58
 
59
+ private static bool GetBoolParam(Dictionary<string, object> parameters, string key, bool defaultValue)
60
+ {
61
+ if (parameters != null && parameters.TryGetValue(key, out var valueObj) && valueObj is bool value)
62
+ return value;
63
+
64
+ return defaultValue;
65
+ }
66
+
67
+ private static void SaveDirtyScenesIfRequested(bool saveDirtyScenes, bool discardUntitled)
68
+ {
69
+ if (!saveDirtyScenes)
70
+ return;
71
+
72
+ var requiresUntitledDiscard = false;
73
+
74
+ for (var index = 0; index < SceneManager.sceneCount; index++)
75
+ {
76
+ var scene = SceneManager.GetSceneAt(index);
77
+ if (!scene.isLoaded || !scene.isDirty)
78
+ continue;
79
+
80
+ if (string.IsNullOrEmpty(scene.path))
81
+ {
82
+ if (!discardUntitled)
83
+ throw new System.InvalidOperationException("Dirty untitled scene cannot be auto-saved. Retry with discardUntitled=true.");
84
+
85
+ requiresUntitledDiscard = true;
86
+ continue;
87
+ }
88
+
89
+ if (!EditorSceneManager.SaveScene(scene))
90
+ throw new System.InvalidOperationException($"Failed to auto-save dirty scene: {scene.path}");
91
+ }
92
+
93
+ if (requiresUntitledDiscard)
94
+ EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
95
+ }
96
+
56
97
  private static object HandleActive(string paramsJson)
57
98
  {
58
99
  var scene = SceneManager.GetActiveScene();
@@ -55,7 +55,21 @@ namespace UCP.Bridge
55
55
  executionSettings.filters[0].testNames = new[] { filter };
56
56
  }
57
57
 
58
- s_api.Execute(executionSettings);
58
+ var shouldWaitForPlayModeExit =
59
+ testMode == TestMode.EditMode &&
60
+ (EditorApplication.isPlaying || EditorApplication.isPlayingOrWillChangePlaymode);
61
+
62
+ if (shouldWaitForPlayModeExit)
63
+ {
64
+ if (EditorApplication.isPlaying)
65
+ EditorApplication.isPlaying = false;
66
+
67
+ ExecuteWhenReady(executionSettings, testMode, EditorApplication.timeSinceStartup + 30.0);
68
+ }
69
+ else
70
+ {
71
+ s_api.Execute(executionSettings);
72
+ }
59
73
 
60
74
  // Tests run asynchronously in Unity. We return immediately with a pending status.
61
75
  // Results will be sent as notifications when complete.
@@ -63,7 +77,53 @@ namespace UCP.Bridge
63
77
  {
64
78
  ["status"] = "started",
65
79
  ["mode"] = mode,
66
- ["message"] = "Tests started. Results will arrive as notifications."
80
+ ["message"] = shouldWaitForPlayModeExit
81
+ ? "Edit-mode tests queued. Waiting for Unity to exit play mode before starting."
82
+ : "Tests started. Results will arrive as notifications."
83
+ };
84
+ }
85
+
86
+ private static void ExecuteWhenReady(ExecutionSettings settings, TestMode mode, double deadline)
87
+ {
88
+ EditorApplication.delayCall += () =>
89
+ {
90
+ var stillInPlayMode = EditorApplication.isPlaying || EditorApplication.isPlayingOrWillChangePlaymode;
91
+ if (mode == TestMode.EditMode && stillInPlayMode)
92
+ {
93
+ if (EditorApplication.timeSinceStartup > deadline)
94
+ {
95
+ BridgeServer.BroadcastNotification("tests/result", new Dictionary<string, object>
96
+ {
97
+ ["summary"] = new Dictionary<string, object>
98
+ {
99
+ ["total"] = 1,
100
+ ["passed"] = 0,
101
+ ["failed"] = 1,
102
+ ["skipped"] = 0,
103
+ ["duration"] = 0.0
104
+ },
105
+ ["tests"] = new List<object>
106
+ {
107
+ new Dictionary<string, object>
108
+ {
109
+ ["name"] = "UCP.Bridge.Tests.PlayModeExitGuard",
110
+ ["status"] = "failed",
111
+ ["duration"] = 0.0,
112
+ ["message"] = "Timed out waiting for Unity to exit play mode before running edit-mode tests."
113
+ }
114
+ }
115
+ });
116
+
117
+ if (s_api != null && s_collector != null)
118
+ s_api.UnregisterCallbacks(s_collector);
119
+ return;
120
+ }
121
+
122
+ ExecuteWhenReady(settings, mode, deadline);
123
+ return;
124
+ }
125
+
126
+ s_api.Execute(settings);
67
127
  };
68
128
  }
69
129
 
@@ -1,15 +1,21 @@
1
1
  using System;
2
+ using System.Collections;
2
3
  using System.Collections.Generic;
4
+ using System.Text.RegularExpressions;
3
5
  using NUnit.Framework;
4
6
  using UnityEditor;
5
7
  using UnityEditor.SceneManagement;
6
8
  using UnityEngine;
9
+ using UnityEngine.TestTools;
7
10
 
8
11
  namespace UCP.Bridge.Tests
9
12
  {
10
13
  public class ControllerSmokeTests
11
14
  {
12
15
  private const string TempAssetPath = "Assets/UcpControllerSmoke.asset";
16
+ private const string TempPrefabPath = "Assets/UcpControllerSmoke.prefab";
17
+ private const string TempMaterialPath = "Assets/UcpControllerSmoke.mat";
18
+ private const string TempTextPath = "Assets/UcpControllerSmoke.txt";
13
19
 
14
20
  private CommandRouter _router;
15
21
 
@@ -20,8 +26,18 @@ namespace UCP.Bridge.Tests
20
26
  SnapshotController.Register(_router);
21
27
  AssetController.Register(_router);
22
28
  LogsController.Register(_router);
29
+ HierarchyController.Register(_router);
30
+ PropertyController.Register(_router);
31
+ FileController.Register(_router);
32
+ MaterialController.Register(_router);
33
+ PrefabController.Register(_router);
34
+ BuildController.Register(_router);
35
+ EditorSettingsController.Register(_router);
23
36
  EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
24
37
  DeleteTempAsset();
38
+ DeleteTempPrefab();
39
+ DeleteTempMaterial();
40
+ DeleteTempTextFile();
25
41
  LogsController.ClearHistoryForTests();
26
42
  }
27
43
 
@@ -29,6 +45,9 @@ namespace UCP.Bridge.Tests
29
45
  public void TearDown()
30
46
  {
31
47
  DeleteTempAsset();
48
+ DeleteTempPrefab();
49
+ DeleteTempMaterial();
50
+ DeleteTempTextFile();
32
51
  LogsController.ClearHistoryForTests();
33
52
  EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
34
53
  }
@@ -76,17 +95,18 @@ namespace UCP.Bridge.Tests
76
95
  var response = _router.Dispatch(
77
96
  "asset/search",
78
97
  1,
79
- "{\"type\":\"SearchNestedAsset\",\"name\":\"SmokeNested\",\"path\":\"Assets\",\"maxResults\":10}"
98
+ "{\"name\":\"SmokeNested\",\"path\":\"Assets\",\"maxResults\":10}"
80
99
  );
81
100
 
82
101
  Assert.That(response.error, Is.Null);
83
102
 
84
103
  var result = (Dictionary<string, object>)response.result;
85
- Assert.That(Convert.ToInt32(result["total"]), Is.EqualTo(1));
86
- Assert.That(Convert.ToInt32(result["returned"]), Is.EqualTo(1));
104
+ Assert.That(Convert.ToInt32(result["total"]), Is.GreaterThanOrEqualTo(1));
105
+ Assert.That(Convert.ToInt32(result["returned"]), Is.GreaterThanOrEqualTo(1));
87
106
 
88
107
  var matches = (List<object>)result["results"];
89
- var match = (Dictionary<string, object>)matches[0];
108
+ var match = FindAssetMatch(matches, TempAssetPath, "SmokeNested");
109
+ Assert.That(match, Is.Not.Null);
90
110
  Assert.That(match["path"], Is.EqualTo(TempAssetPath));
91
111
  Assert.That(match["type"], Is.EqualTo("SearchNestedAsset"));
92
112
  Assert.That(match["name"], Is.EqualTo("SmokeNested"));
@@ -174,6 +194,214 @@ namespace UCP.Bridge.Tests
174
194
  Assert.That(result["stackTrace"], Is.EqualTo("stack line 1\nstack line 2"));
175
195
  }
176
196
 
197
+ [Test]
198
+ public void ObjectLifecycle_CreateMutateAndDelete_WorksEndToEnd()
199
+ {
200
+ var create = _router.Dispatch("object/create", 1, "{\"name\":\"SmokeObject\"}");
201
+ Assert.That(create.error, Is.Null);
202
+
203
+ var createResult = (Dictionary<string, object>)create.result;
204
+ var instanceId = Convert.ToInt32(createResult["instanceId"]);
205
+
206
+ var rename = _router.Dispatch("object/set-name", 1, "{\"instanceId\":" + instanceId + ",\"name\":\"RenamedSmoke\"}");
207
+ Assert.That(rename.error, Is.Null);
208
+
209
+ var deactivate = _router.Dispatch("object/set-active", 1, "{\"instanceId\":" + instanceId + ",\"active\":false}");
210
+ Assert.That(deactivate.error, Is.Null);
211
+ var deactivateResult = (Dictionary<string, object>)deactivate.result;
212
+ Assert.That(Convert.ToBoolean(deactivateResult["active"]), Is.False);
213
+
214
+ var activate = _router.Dispatch("object/set-active", 1, "{\"instanceId\":" + instanceId + ",\"active\":true}");
215
+ Assert.That(activate.error, Is.Null);
216
+
217
+ var addComponent = _router.Dispatch("object/add-component", 1, "{\"instanceId\":" + instanceId + ",\"type\":\"BoxCollider\"}");
218
+ Assert.That(addComponent.error, Is.Null);
219
+
220
+ var setPosition = _router.Dispatch(
221
+ "object/set-property",
222
+ 1,
223
+ "{\"instanceId\":" + instanceId + ",\"component\":\"Transform\",\"property\":\"m_LocalPosition\",\"value\":[1,2,3]}"
224
+ );
225
+ Assert.That(setPosition.error, Is.Null);
226
+
227
+ var getPosition = _router.Dispatch(
228
+ "object/get-property",
229
+ 1,
230
+ "{\"instanceId\":" + instanceId + ",\"component\":\"Transform\",\"property\":\"m_LocalPosition\"}"
231
+ );
232
+ Assert.That(getPosition.error, Is.Null);
233
+
234
+ var updated = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
235
+ Assert.That(updated, Is.Not.Null);
236
+ var localPosition = updated.transform.localPosition;
237
+ Assert.That(localPosition.x, Is.EqualTo(1f).Within(0.001f));
238
+ Assert.That(localPosition.y, Is.EqualTo(2f).Within(0.001f));
239
+ Assert.That(localPosition.z, Is.EqualTo(3f).Within(0.001f));
240
+
241
+ var removeComponent = _router.Dispatch("object/remove-component", 1, "{\"instanceId\":" + instanceId + ",\"type\":\"BoxCollider\"}");
242
+ Assert.That(removeComponent.error, Is.Null);
243
+
244
+ var delete = _router.Dispatch("object/delete", 1, "{\"instanceId\":" + instanceId + "}");
245
+ Assert.That(delete.error, Is.Null);
246
+ Assert.That(EditorUtility.InstanceIDToObject(instanceId), Is.Null);
247
+ }
248
+
249
+ [Test]
250
+ public void FileController_WritePatchRead_AndRejectsPathTraversal()
251
+ {
252
+ var write = _router.Dispatch("file/write", 1, "{\"path\":\"Assets/UcpControllerSmoke.txt\",\"content\":\"hello smoke\"}");
253
+ Assert.That(write.error, Is.Null);
254
+
255
+ var patch = _router.Dispatch(
256
+ "file/patch",
257
+ 1,
258
+ "{\"path\":\"Assets/UcpControllerSmoke.txt\",\"patch\":{\"find\":\"smoke\",\"replace\":\"patched\"}}"
259
+ );
260
+ Assert.That(patch.error, Is.Null);
261
+
262
+ var read = _router.Dispatch("file/read", 1, "{\"path\":\"Assets/UcpControllerSmoke.txt\"}");
263
+ Assert.That(read.error, Is.Null);
264
+ var readResult = (Dictionary<string, object>)read.result;
265
+ Assert.That(readResult["content"].ToString(), Is.EqualTo("hello patched"));
266
+
267
+ LogAssert.Expect(LogType.Error, new Regex("\\[UCP\\] Error handling 'file/read':"));
268
+ var traversal = _router.Dispatch("file/read", 1, "{\"path\":\"../outside.txt\"}");
269
+ Assert.That(traversal.error, Is.Not.Null);
270
+ }
271
+
272
+ [Test]
273
+ public void MaterialController_SetAndGetFloatProperty_RoundTrips()
274
+ {
275
+ var shader = Shader.Find("Standard") ?? Shader.Find("Universal Render Pipeline/Lit");
276
+ Assert.That(shader, Is.Not.Null);
277
+
278
+ var propertyName = FindFirstFloatOrRangeProperty(shader);
279
+ Assert.That(propertyName, Is.Not.Null.And.Not.Empty);
280
+
281
+ var material = new Material(shader) { name = "UcpControllerSmokeMat" };
282
+ AssetDatabase.CreateAsset(material, TempMaterialPath);
283
+ AssetDatabase.SaveAssets();
284
+
285
+ var set = _router.Dispatch(
286
+ "material/set-property",
287
+ 1,
288
+ "{\"path\":\"Assets/UcpControllerSmoke.mat\",\"property\":\"" + propertyName + "\",\"value\":0.42}"
289
+ );
290
+ Assert.That(set.error, Is.Null);
291
+
292
+ var get = _router.Dispatch(
293
+ "material/get-property",
294
+ 1,
295
+ "{\"path\":\"Assets/UcpControllerSmoke.mat\",\"property\":\"" + propertyName + "\"}"
296
+ );
297
+ Assert.That(get.error, Is.Null);
298
+
299
+ var getResult = (Dictionary<string, object>)get.result;
300
+ var value = Convert.ToSingle(getResult["value"]);
301
+ Assert.That(value, Is.EqualTo(0.42f).Within(0.001f));
302
+ }
303
+
304
+ [Test]
305
+ public void PrefabController_CreateStatusOverridesAndUnpack_Works()
306
+ {
307
+ var create = _router.Dispatch("object/create", 1, "{\"name\":\"PrefabSource\"}");
308
+ Assert.That(create.error, Is.Null);
309
+ var sourceId = Convert.ToInt32(((Dictionary<string, object>)create.result)["instanceId"]);
310
+
311
+ var createPrefab = _router.Dispatch(
312
+ "prefab/create",
313
+ 1,
314
+ "{\"instanceId\":" + sourceId + ",\"path\":\"Assets/UcpControllerSmoke.prefab\"}"
315
+ );
316
+ Assert.That(createPrefab.error, Is.Null);
317
+
318
+ var instantiate = _router.Dispatch(
319
+ "object/instantiate",
320
+ 1,
321
+ "{\"prefab\":\"Assets/UcpControllerSmoke.prefab\",\"name\":\"PrefabInstance\"}"
322
+ );
323
+ Assert.That(instantiate.error, Is.Null);
324
+ var instanceId = Convert.ToInt32(((Dictionary<string, object>)instantiate.result)["instanceId"]);
325
+
326
+ var status = _router.Dispatch("prefab/status", 1, "{\"instanceId\":" + instanceId + "}");
327
+ Assert.That(status.error, Is.Null);
328
+ var statusResult = (Dictionary<string, object>)status.result;
329
+ Assert.That(Convert.ToBoolean(statusResult["isInstance"]), Is.True);
330
+
331
+ var mutate = _router.Dispatch(
332
+ "object/set-property",
333
+ 1,
334
+ "{\"instanceId\":" + instanceId + ",\"component\":\"Transform\",\"property\":\"m_LocalPosition\",\"value\":[2,0,0]}"
335
+ );
336
+ Assert.That(mutate.error, Is.Null);
337
+
338
+ var overrides = _router.Dispatch("prefab/overrides", 1, "{\"instanceId\":" + instanceId + "}");
339
+ Assert.That(overrides.error, Is.Null);
340
+ var overridesResult = (Dictionary<string, object>)overrides.result;
341
+ var modifications = (List<object>)overridesResult["propertyModifications"];
342
+ Assert.That(modifications.Count, Is.GreaterThanOrEqualTo(1));
343
+
344
+ var unpack = _router.Dispatch("prefab/unpack", 1, "{\"instanceId\":" + instanceId + "}");
345
+ Assert.That(unpack.error, Is.Null);
346
+
347
+ var unpackedStatus = _router.Dispatch("prefab/status", 1, "{\"instanceId\":" + instanceId + "}");
348
+ Assert.That(unpackedStatus.error, Is.Null);
349
+ var unpackedStatusResult = (Dictionary<string, object>)unpackedStatus.result;
350
+ Assert.That(Convert.ToBoolean(unpackedStatusResult["isInstance"]), Is.False);
351
+ }
352
+
353
+ [Test]
354
+ public void SettingsAndBuildControllers_RoundTripWithoutSideEffects()
355
+ {
356
+ var playerSettings = _router.Dispatch("settings/player", 1, "{}");
357
+ Assert.That(playerSettings.error, Is.Null);
358
+ var settingsResult = (Dictionary<string, object>)playerSettings.result;
359
+ var originalProductName = settingsResult["productName"].ToString();
360
+
361
+ var setPlayer = _router.Dispatch(
362
+ "settings/player-set",
363
+ 1,
364
+ "{\"key\":\"productName\",\"value\":\"UcpQaProduct\"}"
365
+ );
366
+ Assert.That(setPlayer.error, Is.Null);
367
+
368
+ var verifyPlayer = _router.Dispatch("settings/player", 1, "{}");
369
+ Assert.That(verifyPlayer.error, Is.Null);
370
+ var verifyResult = (Dictionary<string, object>)verifyPlayer.result;
371
+ Assert.That(verifyResult["productName"].ToString(), Is.EqualTo("UcpQaProduct"));
372
+
373
+ var restorePlayer = _router.Dispatch(
374
+ "settings/player-set",
375
+ 1,
376
+ "{\"key\":\"productName\",\"value\":\"" + originalProductName + "\"}"
377
+ );
378
+ Assert.That(restorePlayer.error, Is.Null);
379
+
380
+ var scenes = _router.Dispatch("build/scenes", 1, "{}");
381
+ Assert.That(scenes.error, Is.Null);
382
+ var scenesResult = (Dictionary<string, object>)scenes.result;
383
+ var currentScenes = (List<object>)scenesResult["scenes"];
384
+ Assert.That(currentScenes.Count, Is.GreaterThanOrEqualTo(1));
385
+
386
+ var firstScene = (Dictionary<string, object>)currentScenes[0];
387
+ var firstScenePath = firstScene["path"].ToString();
388
+
389
+ var setScenes = _router.Dispatch(
390
+ "build/set-scenes",
391
+ 1,
392
+ "{\"scenes\":[\"" + EscapeForJson(firstScenePath) + "\"]}"
393
+ );
394
+ Assert.That(setScenes.error, Is.Null);
395
+
396
+ var defines = _router.Dispatch("build/defines", 1, "{}");
397
+ Assert.That(defines.error, Is.Null);
398
+ var definesResult = (Dictionary<string, object>)defines.result;
399
+ var originalDefines = definesResult["defines"].ToString();
400
+
401
+ var setDefines = _router.Dispatch("build/set-defines", 1, "{\"defines\":\"" + EscapeForJson(originalDefines) + "\"}");
402
+ Assert.That(setDefines.error, Is.Null);
403
+ }
404
+
177
405
  private static void DeleteTempAsset()
178
406
  {
179
407
  if (AssetDatabase.LoadMainAssetAtPath(TempAssetPath) != null)
@@ -183,6 +411,69 @@ namespace UCP.Bridge.Tests
183
411
  }
184
412
  }
185
413
 
414
+ private static void DeleteTempPrefab()
415
+ {
416
+ if (AssetDatabase.LoadMainAssetAtPath(TempPrefabPath) != null)
417
+ {
418
+ AssetDatabase.DeleteAsset(TempPrefabPath);
419
+ AssetDatabase.SaveAssets();
420
+ }
421
+ }
422
+
423
+ private static void DeleteTempMaterial()
424
+ {
425
+ if (AssetDatabase.LoadMainAssetAtPath(TempMaterialPath) != null)
426
+ {
427
+ AssetDatabase.DeleteAsset(TempMaterialPath);
428
+ AssetDatabase.SaveAssets();
429
+ }
430
+ }
431
+
432
+ private static void DeleteTempTextFile()
433
+ {
434
+ if (AssetDatabase.LoadMainAssetAtPath(TempTextPath) != null)
435
+ {
436
+ AssetDatabase.DeleteAsset(TempTextPath);
437
+ AssetDatabase.SaveAssets();
438
+ }
439
+ }
440
+
441
+ private static string FindFirstFloatOrRangeProperty(Shader shader)
442
+ {
443
+ for (var index = 0; index < shader.GetPropertyCount(); index++)
444
+ {
445
+ var propertyType = shader.GetPropertyType(index);
446
+ if (propertyType == UnityEngine.Rendering.ShaderPropertyType.Float
447
+ || propertyType == UnityEngine.Rendering.ShaderPropertyType.Range)
448
+ {
449
+ return shader.GetPropertyName(index);
450
+ }
451
+ }
452
+
453
+ return null;
454
+ }
455
+
456
+ private static string EscapeForJson(string value)
457
+ {
458
+ return value
459
+ .Replace("\\", "\\\\")
460
+ .Replace("\"", "\\\"");
461
+ }
462
+
463
+ private static Dictionary<string, object> FindAssetMatch(List<object> matches, string expectedPath, string expectedName)
464
+ {
465
+ foreach (var entry in matches)
466
+ {
467
+ var match = (Dictionary<string, object>)entry;
468
+ var path = match.ContainsKey("path") ? match["path"].ToString() : string.Empty;
469
+ var name = match.ContainsKey("name") ? match["name"].ToString() : string.Empty;
470
+ if (path == expectedPath && name == expectedName)
471
+ return match;
472
+ }
473
+
474
+ return null;
475
+ }
476
+
186
477
  private sealed class SearchRootAsset : ScriptableObject
187
478
  {
188
479
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.ucp.bridge",
3
- "version": "0.2.3",
3
+ "version": "0.3.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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mflrevan/ucp",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Unity Control Protocol — CLI for programmatic Unity Editor control",
5
5
  "license": "MIT",
6
6
  "repository": {