@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 +4 -2
- package/bridge/com.ucp.bridge/CHANGELOG.md +15 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +46 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +42 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +62 -2
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +295 -4
- 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.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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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"] =
|
|
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
|
-
"{\"
|
|
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.
|
|
86
|
-
Assert.That(Convert.ToInt32(result["returned"]), Is.
|
|
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 = (
|
|
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
|
}
|