@mflrevan/ucp 0.3.0 → 0.3.2
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 +2 -2
- package/bridge/com.ucp.bridge/CHANGELOG.md +27 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +4 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +50 -19
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorController.cs +18 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorSettingsController.cs +4 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +3 -6
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +93 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +5 -23
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +108 -3
- package/bridge/com.ucp.bridge/package.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mflrevan/ucp
|
|
2
2
|
|
|
3
|
-
Version `0.3.
|
|
3
|
+
Version `0.3.2` 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
|
|
|
@@ -49,7 +49,7 @@ Or add this to `Packages/manifest.json`:
|
|
|
49
49
|
```json
|
|
50
50
|
{
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"com.ucp.bridge": "https://github.com/mflRevan/unity-control-protocol.git?path=unity-package/com.ucp.bridge#v0.3.
|
|
52
|
+
"com.ucp.bridge": "https://github.com/mflRevan/unity-control-protocol.git?path=unity-package/com.ucp.bridge#v0.3.2"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
```
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.2] - 2026-03-14
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added `editor/quit` so the CLI can request graceful Unity editor shutdown before falling back to OS-level close/terminate behavior.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Bridge server registration now includes editor lifecycle RPC handlers alongside the existing play, compile, scene, asset, and build controllers.
|
|
12
|
+
|
|
13
|
+
## [0.3.1] - 2026-03-14
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added `asset/write-batch` for multi-field serialized asset updates in one bridge call.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Player settings now expose `defaultIsNativeResolution` so installer automation can reconcile live editor state as well as on-disk project settings.
|
|
22
|
+
- Object reference payloads now include asset `path` and `guid` when available.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed buffered log searches by applying regex filtering before count truncation.
|
|
27
|
+
- Fixed buffered log list requests being capped to 10 returned entries regardless of requested `count`.
|
|
28
|
+
- Fixed serialized object reference writes silently accepting unresolved references in both object and asset controllers.
|
|
29
|
+
|
|
3
30
|
## [0.3.0] - 2026-03-13
|
|
4
31
|
|
|
5
32
|
### Added
|
|
@@ -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.3.
|
|
29
|
+
private const string ProtocolVersion = "0.3.2";
|
|
30
30
|
|
|
31
31
|
private static TcpListener s_listener;
|
|
32
32
|
private static CancellationTokenSource s_cts;
|
|
@@ -108,6 +108,9 @@ namespace UCP.Bridge
|
|
|
108
108
|
// Compilation
|
|
109
109
|
CompilationController.Register(s_router);
|
|
110
110
|
|
|
111
|
+
// Editor lifecycle
|
|
112
|
+
EditorController.Register(s_router);
|
|
113
|
+
|
|
111
114
|
// Scenes
|
|
112
115
|
SceneController.Register(s_router);
|
|
113
116
|
|
|
@@ -14,6 +14,7 @@ namespace UCP.Bridge
|
|
|
14
14
|
router.Register("asset/info", HandleInfo);
|
|
15
15
|
router.Register("asset/read", HandleReadScriptableObject);
|
|
16
16
|
router.Register("asset/write", HandleWriteScriptableObject);
|
|
17
|
+
router.Register("asset/write-batch", HandleWriteScriptableObjectBatch);
|
|
17
18
|
router.Register("asset/create-so", HandleCreateScriptableObject);
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -207,15 +208,44 @@ namespace UCP.Bridge
|
|
|
207
208
|
|
|
208
209
|
string fieldName = fieldObj.ToString();
|
|
209
210
|
var so = new SerializedObject(asset);
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
Undo.RecordObject(asset, $"UCP Write {fieldName}");
|
|
212
|
+
WriteFieldValue(so, asset, fieldName, p["value"]);
|
|
213
|
+
so.ApplyModifiedProperties();
|
|
214
|
+
EditorUtility.SetDirty(asset);
|
|
215
|
+
AssetDatabase.SaveAssetIfDirty(asset);
|
|
216
|
+
so.Dispose();
|
|
217
|
+
|
|
218
|
+
return new Dictionary<string, object>
|
|
212
219
|
{
|
|
213
|
-
|
|
214
|
-
|
|
220
|
+
["status"] = "ok",
|
|
221
|
+
["path"] = assetPath,
|
|
222
|
+
["field"] = fieldName
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private static object HandleWriteScriptableObjectBatch(string paramsJson)
|
|
227
|
+
{
|
|
228
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
229
|
+
if (p == null || !p.TryGetValue("path", out var pathObj))
|
|
230
|
+
throw new ArgumentException("Missing 'path' parameter");
|
|
231
|
+
if (!p.TryGetValue("values", out var valuesObj) || !(valuesObj is Dictionary<string, object> values))
|
|
232
|
+
throw new ArgumentException("Missing 'values' parameter");
|
|
233
|
+
|
|
234
|
+
string assetPath = pathObj.ToString();
|
|
235
|
+
var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
|
|
236
|
+
if (asset == null)
|
|
237
|
+
throw new ArgumentException($"Asset not found: {assetPath}");
|
|
238
|
+
|
|
239
|
+
var so = new SerializedObject(asset);
|
|
240
|
+
Undo.RecordObject(asset, $"UCP Batch Write {asset.name}");
|
|
241
|
+
|
|
242
|
+
var fields = new List<object>();
|
|
243
|
+
foreach (var entry in values)
|
|
244
|
+
{
|
|
245
|
+
WriteFieldValue(so, asset, entry.Key, entry.Value);
|
|
246
|
+
fields.Add(entry.Key);
|
|
215
247
|
}
|
|
216
248
|
|
|
217
|
-
Undo.RecordObject(asset, $"UCP Write {fieldName}");
|
|
218
|
-
WriteSerializedValue(prop, p["value"]);
|
|
219
249
|
so.ApplyModifiedProperties();
|
|
220
250
|
EditorUtility.SetDirty(asset);
|
|
221
251
|
AssetDatabase.SaveAssetIfDirty(asset);
|
|
@@ -225,7 +255,7 @@ namespace UCP.Bridge
|
|
|
225
255
|
{
|
|
226
256
|
["status"] = "ok",
|
|
227
257
|
["path"] = assetPath,
|
|
228
|
-
["
|
|
258
|
+
["fields"] = fields
|
|
229
259
|
};
|
|
230
260
|
}
|
|
231
261
|
|
|
@@ -381,14 +411,7 @@ namespace UCP.Bridge
|
|
|
381
411
|
var c = prop.colorValue;
|
|
382
412
|
return new List<object> { (double)c.r, (double)c.g, (double)c.b, (double)c.a };
|
|
383
413
|
case SerializedPropertyType.ObjectReference:
|
|
384
|
-
|
|
385
|
-
return new Dictionary<string, object>
|
|
386
|
-
{
|
|
387
|
-
["instanceId"] = prop.objectReferenceValue.GetInstanceID(),
|
|
388
|
-
["name"] = prop.objectReferenceValue.name,
|
|
389
|
-
["type"] = prop.objectReferenceValue.GetType().Name
|
|
390
|
-
};
|
|
391
|
-
return null;
|
|
414
|
+
return ObjectReferenceResolver.Serialize(prop.objectReferenceValue);
|
|
392
415
|
case SerializedPropertyType.Enum:
|
|
393
416
|
return prop.enumValueIndex < prop.enumDisplayNames.Length
|
|
394
417
|
? prop.enumDisplayNames[prop.enumValueIndex]
|
|
@@ -483,10 +506,9 @@ namespace UCP.Bridge
|
|
|
483
506
|
prop.quaternionValue = new Quaternion(Convert.ToSingle(qArr[0]), Convert.ToSingle(qArr[1]), Convert.ToSingle(qArr[2]), Convert.ToSingle(qArr[3]));
|
|
484
507
|
break;
|
|
485
508
|
case SerializedPropertyType.ObjectReference:
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
prop.objectReferenceValue = EditorUtility.InstanceIDToObject(Convert.ToInt32(refId));
|
|
509
|
+
prop.objectReferenceValue = ObjectReferenceResolver.Resolve(value, prop.displayName);
|
|
510
|
+
if (value != null && prop.objectReferenceValue == null)
|
|
511
|
+
throw new ArgumentException($"Unable to assign object reference to '{prop.displayName}'");
|
|
490
512
|
break;
|
|
491
513
|
case SerializedPropertyType.LayerMask:
|
|
492
514
|
prop.intValue = Convert.ToInt32(value);
|
|
@@ -495,5 +517,14 @@ namespace UCP.Bridge
|
|
|
495
517
|
throw new ArgumentException($"Cannot write property of type {prop.propertyType}");
|
|
496
518
|
}
|
|
497
519
|
}
|
|
520
|
+
|
|
521
|
+
private static void WriteFieldValue(SerializedObject serializedObject, UnityEngine.Object asset, string fieldName, object value)
|
|
522
|
+
{
|
|
523
|
+
var prop = serializedObject.FindProperty(fieldName);
|
|
524
|
+
if (prop == null)
|
|
525
|
+
throw new ArgumentException($"Field '{fieldName}' not found on {asset.GetType().Name}");
|
|
526
|
+
|
|
527
|
+
WriteSerializedValue(prop, value);
|
|
528
|
+
}
|
|
498
529
|
}
|
|
499
530
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
using UnityEditor;
|
|
2
|
+
|
|
3
|
+
namespace UCP.Bridge
|
|
4
|
+
{
|
|
5
|
+
public static class EditorController
|
|
6
|
+
{
|
|
7
|
+
public static void Register(CommandRouter router)
|
|
8
|
+
{
|
|
9
|
+
router.Register("editor/quit", HandleQuit);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private static object HandleQuit(string paramsJson)
|
|
13
|
+
{
|
|
14
|
+
EditorApplication.delayCall += () => EditorApplication.Exit(0);
|
|
15
|
+
return new { status = "ok", message = "Unity editor shutdown requested" };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -30,7 +30,7 @@ namespace UCP.Bridge
|
|
|
30
30
|
["companyName"] = PlayerSettings.companyName,
|
|
31
31
|
["productName"] = PlayerSettings.productName,
|
|
32
32
|
["bundleVersion"] = PlayerSettings.bundleVersion,
|
|
33
|
-
["
|
|
33
|
+
["defaultIsNativeResolution"] = PlayerSettings.defaultIsNativeResolution,
|
|
34
34
|
["runInBackground"] = PlayerSettings.runInBackground,
|
|
35
35
|
["colorSpace"] = PlayerSettings.colorSpace.ToString(),
|
|
36
36
|
["graphicsApi"] = PlayerSettings.GetGraphicsAPIs(EditorUserBuildSettings.activeBuildTarget)?[0].ToString() ?? "Unknown",
|
|
@@ -67,6 +67,9 @@ namespace UCP.Bridge
|
|
|
67
67
|
case "runInBackground":
|
|
68
68
|
PlayerSettings.runInBackground = Convert.ToBoolean(value);
|
|
69
69
|
break;
|
|
70
|
+
case "defaultIsNativeResolution":
|
|
71
|
+
PlayerSettings.defaultIsNativeResolution = Convert.ToBoolean(value);
|
|
72
|
+
break;
|
|
70
73
|
case "defaultScreenWidth":
|
|
71
74
|
PlayerSettings.defaultScreenWidth = Convert.ToInt32(value);
|
|
72
75
|
break;
|
|
@@ -9,7 +9,6 @@ namespace UCP.Bridge
|
|
|
9
9
|
public static class LogsController
|
|
10
10
|
{
|
|
11
11
|
private const int MaxHistoryEntries = 2000;
|
|
12
|
-
private const int MaxBulkResults = 10;
|
|
13
12
|
private const int DefaultSearchWindow = 200;
|
|
14
13
|
private const int MaxPreviewLength = 200;
|
|
15
14
|
|
|
@@ -145,8 +144,6 @@ namespace UCP.Bridge
|
|
|
145
144
|
if (!string.IsNullOrEmpty(query.Level))
|
|
146
145
|
candidates = candidates.Where(entry => PassesLevel(entry.Level, query.Level));
|
|
147
146
|
|
|
148
|
-
candidates = candidates.OrderByDescending(entry => entry.Id).Take(query.Count);
|
|
149
|
-
|
|
150
147
|
if (query.Regex != null)
|
|
151
148
|
{
|
|
152
149
|
candidates = candidates.Where(entry =>
|
|
@@ -155,14 +152,14 @@ namespace UCP.Bridge
|
|
|
155
152
|
);
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
var allMatches = candidates.ToList();
|
|
159
|
-
var returned = allMatches.Take(
|
|
155
|
+
var allMatches = candidates.OrderByDescending(entry => entry.Id).ToList();
|
|
156
|
+
var returned = allMatches.Take(query.Count).Select(SerializeSummary).ToList();
|
|
160
157
|
|
|
161
158
|
return new LogQueryResult
|
|
162
159
|
{
|
|
163
160
|
Total = allMatches.Count,
|
|
164
161
|
Returned = returned,
|
|
165
|
-
Truncated = allMatches.Count >
|
|
162
|
+
Truncated = allMatches.Count > returned.Count
|
|
166
163
|
};
|
|
167
164
|
}
|
|
168
165
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using UnityEditor;
|
|
4
|
+
using UnityEngine;
|
|
5
|
+
|
|
6
|
+
namespace UCP.Bridge
|
|
7
|
+
{
|
|
8
|
+
internal static class ObjectReferenceResolver
|
|
9
|
+
{
|
|
10
|
+
public static Dictionary<string, object> Serialize(UnityEngine.Object obj)
|
|
11
|
+
{
|
|
12
|
+
if (obj == null)
|
|
13
|
+
return null;
|
|
14
|
+
|
|
15
|
+
var result = new Dictionary<string, object>
|
|
16
|
+
{
|
|
17
|
+
["instanceId"] = obj.GetInstanceID(),
|
|
18
|
+
["name"] = obj.name,
|
|
19
|
+
["type"] = obj.GetType().Name
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
var assetPath = AssetDatabase.GetAssetPath(obj);
|
|
23
|
+
if (!string.IsNullOrEmpty(assetPath))
|
|
24
|
+
{
|
|
25
|
+
result["path"] = assetPath;
|
|
26
|
+
var guid = AssetDatabase.AssetPathToGUID(assetPath);
|
|
27
|
+
if (!string.IsNullOrEmpty(guid))
|
|
28
|
+
result["guid"] = guid;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public static UnityEngine.Object Resolve(object value, string propertyName)
|
|
35
|
+
{
|
|
36
|
+
if (value == null)
|
|
37
|
+
return null;
|
|
38
|
+
|
|
39
|
+
if (value is Dictionary<string, object> reference)
|
|
40
|
+
{
|
|
41
|
+
if (reference.TryGetValue("instanceId", out var instanceId) && instanceId != null)
|
|
42
|
+
return ResolveByInstanceId(Convert.ToInt32(instanceId), propertyName);
|
|
43
|
+
|
|
44
|
+
if (reference.TryGetValue("path", out var path) && path != null)
|
|
45
|
+
return ResolveByPath(path.ToString(), propertyName);
|
|
46
|
+
|
|
47
|
+
if (reference.TryGetValue("guid", out var guid) && guid != null)
|
|
48
|
+
return ResolveByGuid(guid.ToString(), propertyName);
|
|
49
|
+
|
|
50
|
+
throw new ArgumentException($"Object reference for '{propertyName}' must include instanceId, path, or guid");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (value is string stringValue)
|
|
54
|
+
{
|
|
55
|
+
return stringValue.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)
|
|
56
|
+
? ResolveByPath(stringValue, propertyName)
|
|
57
|
+
: ResolveByGuid(stringValue, propertyName);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (value is sbyte || value is byte || value is short || value is ushort || value is int || value is uint || value is long || value is ulong)
|
|
61
|
+
return ResolveByInstanceId(Convert.ToInt32(value), propertyName);
|
|
62
|
+
|
|
63
|
+
throw new ArgumentException($"Unsupported object reference value for '{propertyName}'");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static UnityEngine.Object ResolveByInstanceId(int instanceId, string propertyName)
|
|
67
|
+
{
|
|
68
|
+
var resolved = EditorUtility.InstanceIDToObject(instanceId);
|
|
69
|
+
if (resolved == null)
|
|
70
|
+
throw new ArgumentException($"Object reference for '{propertyName}' could not resolve instance id {instanceId}");
|
|
71
|
+
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private static UnityEngine.Object ResolveByPath(string assetPath, string propertyName)
|
|
76
|
+
{
|
|
77
|
+
var resolved = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
|
|
78
|
+
if (resolved == null)
|
|
79
|
+
throw new ArgumentException($"Object reference for '{propertyName}' could not load asset at {assetPath}");
|
|
80
|
+
|
|
81
|
+
return resolved;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private static UnityEngine.Object ResolveByGuid(string guid, string propertyName)
|
|
85
|
+
{
|
|
86
|
+
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
|
87
|
+
if (string.IsNullOrEmpty(assetPath))
|
|
88
|
+
throw new ArgumentException($"Object reference for '{propertyName}' could not resolve guid {guid}");
|
|
89
|
+
|
|
90
|
+
return ResolveByPath(assetPath, propertyName);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -218,14 +218,7 @@ namespace UCP.Bridge
|
|
|
218
218
|
var c = prop.colorValue;
|
|
219
219
|
return new List<object> { (double)c.r, (double)c.g, (double)c.b, (double)c.a };
|
|
220
220
|
case SerializedPropertyType.ObjectReference:
|
|
221
|
-
|
|
222
|
-
return new Dictionary<string, object>
|
|
223
|
-
{
|
|
224
|
-
["instanceId"] = prop.objectReferenceValue.GetInstanceID(),
|
|
225
|
-
["name"] = prop.objectReferenceValue.name,
|
|
226
|
-
["type"] = prop.objectReferenceValue.GetType().Name
|
|
227
|
-
};
|
|
228
|
-
return null;
|
|
221
|
+
return ObjectReferenceResolver.Serialize(prop.objectReferenceValue);
|
|
229
222
|
case SerializedPropertyType.LayerMask:
|
|
230
223
|
return prop.intValue;
|
|
231
224
|
case SerializedPropertyType.Enum:
|
|
@@ -422,15 +415,9 @@ namespace UCP.Bridge
|
|
|
422
415
|
}
|
|
423
416
|
break;
|
|
424
417
|
case SerializedPropertyType.ObjectReference:
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
prop.
|
|
428
|
-
}
|
|
429
|
-
else if (value is Dictionary<string, object> refDict && refDict.TryGetValue("instanceId", out var refId))
|
|
430
|
-
{
|
|
431
|
-
var obj = EditorUtility.InstanceIDToObject(Convert.ToInt32(refId));
|
|
432
|
-
prop.objectReferenceValue = obj;
|
|
433
|
-
}
|
|
418
|
+
prop.objectReferenceValue = ObjectReferenceResolver.Resolve(value, prop.displayName);
|
|
419
|
+
if (value != null && prop.objectReferenceValue == null)
|
|
420
|
+
throw new ArgumentException($"Unable to assign object reference to '{prop.displayName}'");
|
|
434
421
|
break;
|
|
435
422
|
case SerializedPropertyType.LayerMask:
|
|
436
423
|
prop.intValue = Convert.ToInt32(value);
|
|
@@ -457,12 +444,7 @@ namespace UCP.Bridge
|
|
|
457
444
|
if (value is Color c)
|
|
458
445
|
return new List<object> { (double)c.r, (double)c.g, (double)c.b, (double)c.a };
|
|
459
446
|
if (value is UnityEngine.Object uObj)
|
|
460
|
-
return
|
|
461
|
-
{
|
|
462
|
-
["instanceId"] = uObj.GetInstanceID(),
|
|
463
|
-
["name"] = uObj.name,
|
|
464
|
-
["type"] = uObj.GetType().Name
|
|
465
|
-
};
|
|
447
|
+
return ObjectReferenceResolver.Serialize(uObj);
|
|
466
448
|
return value.ToString();
|
|
467
449
|
}
|
|
468
450
|
|
|
@@ -13,6 +13,7 @@ namespace UCP.Bridge.Tests
|
|
|
13
13
|
public class ControllerSmokeTests
|
|
14
14
|
{
|
|
15
15
|
private const string TempAssetPath = "Assets/UcpControllerSmoke.asset";
|
|
16
|
+
private const string TempReferenceAssetPath = "Assets/UcpControllerReference.asset";
|
|
16
17
|
private const string TempPrefabPath = "Assets/UcpControllerSmoke.prefab";
|
|
17
18
|
private const string TempMaterialPath = "Assets/UcpControllerSmoke.mat";
|
|
18
19
|
private const string TempTextPath = "Assets/UcpControllerSmoke.txt";
|
|
@@ -35,6 +36,7 @@ namespace UCP.Bridge.Tests
|
|
|
35
36
|
EditorSettingsController.Register(_router);
|
|
36
37
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
37
38
|
DeleteTempAsset();
|
|
39
|
+
DeleteTempReferenceAsset();
|
|
38
40
|
DeleteTempPrefab();
|
|
39
41
|
DeleteTempMaterial();
|
|
40
42
|
DeleteTempTextFile();
|
|
@@ -45,6 +47,7 @@ namespace UCP.Bridge.Tests
|
|
|
45
47
|
public void TearDown()
|
|
46
48
|
{
|
|
47
49
|
DeleteTempAsset();
|
|
50
|
+
DeleteTempReferenceAsset();
|
|
48
51
|
DeleteTempPrefab();
|
|
49
52
|
DeleteTempMaterial();
|
|
50
53
|
DeleteTempTextFile();
|
|
@@ -114,7 +117,7 @@ namespace UCP.Bridge.Tests
|
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
[Test]
|
|
117
|
-
public void
|
|
120
|
+
public void LogsTail_ReturnsRequestedBufferedCount()
|
|
118
121
|
{
|
|
119
122
|
for (var index = 0; index < 12; index++)
|
|
120
123
|
LogsController.RecordTestLog("info", $"log {index}");
|
|
@@ -125,8 +128,8 @@ namespace UCP.Bridge.Tests
|
|
|
125
128
|
|
|
126
129
|
var result = (Dictionary<string, object>)response.result;
|
|
127
130
|
Assert.That(Convert.ToInt32(result["total"]), Is.EqualTo(12));
|
|
128
|
-
Assert.That(Convert.ToInt32(result["returned"]), Is.EqualTo(
|
|
129
|
-
Assert.That(Convert.ToBoolean(result["truncated"]), Is.
|
|
131
|
+
Assert.That(Convert.ToInt32(result["returned"]), Is.EqualTo(12));
|
|
132
|
+
Assert.That(Convert.ToBoolean(result["truncated"]), Is.False);
|
|
130
133
|
|
|
131
134
|
var logs = (List<object>)result["logs"];
|
|
132
135
|
var first = (Dictionary<string, object>)logs[0];
|
|
@@ -134,6 +137,23 @@ namespace UCP.Bridge.Tests
|
|
|
134
137
|
Assert.That(first.ContainsKey("messagePreview"), Is.True);
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
[Test]
|
|
141
|
+
public void LogsSearch_FiltersBeforeApplyingCount()
|
|
142
|
+
{
|
|
143
|
+
LogsController.RecordTestLog("warning", "Target failed once");
|
|
144
|
+
for (var index = 0; index < 8; index++)
|
|
145
|
+
LogsController.RecordTestLog("info", $"Noise {index}");
|
|
146
|
+
|
|
147
|
+
var response = _router.Dispatch("logs/search", 1, "{\"pattern\":\"Target\",\"count\":1}");
|
|
148
|
+
|
|
149
|
+
Assert.That(response.error, Is.Null);
|
|
150
|
+
|
|
151
|
+
var result = (Dictionary<string, object>)response.result;
|
|
152
|
+
Assert.That(Convert.ToInt32(result["total"]), Is.EqualTo(1));
|
|
153
|
+
Assert.That(Convert.ToInt32(result["returned"]), Is.EqualTo(1));
|
|
154
|
+
Assert.That(Convert.ToBoolean(result["truncated"]), Is.False);
|
|
155
|
+
}
|
|
156
|
+
|
|
137
157
|
[Test]
|
|
138
158
|
public void LogsSearch_UsesRegexAgainstBufferedHistory()
|
|
139
159
|
{
|
|
@@ -246,6 +266,70 @@ namespace UCP.Bridge.Tests
|
|
|
246
266
|
Assert.That(EditorUtility.InstanceIDToObject(instanceId), Is.Null);
|
|
247
267
|
}
|
|
248
268
|
|
|
269
|
+
[Test]
|
|
270
|
+
public void ObjectSetProperty_AssignsObjectReferenceByAssetPath()
|
|
271
|
+
{
|
|
272
|
+
var referencedAsset = ScriptableObject.CreateInstance<SearchRootAsset>();
|
|
273
|
+
referencedAsset.name = "ReferenceAsset";
|
|
274
|
+
AssetDatabase.CreateAsset(referencedAsset, TempReferenceAssetPath);
|
|
275
|
+
AssetDatabase.SaveAssets();
|
|
276
|
+
|
|
277
|
+
var go = new GameObject("ReferenceCarrier");
|
|
278
|
+
var component = go.AddComponent<ReferenceComponent>();
|
|
279
|
+
|
|
280
|
+
var response = _router.Dispatch(
|
|
281
|
+
"object/set-property",
|
|
282
|
+
1,
|
|
283
|
+
"{\"instanceId\":" + go.GetInstanceID() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"" + TempReferenceAssetPath + "\"}}"
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
Assert.That(response.error, Is.Null);
|
|
287
|
+
Assert.That(component.referenceAsset, Is.Not.Null);
|
|
288
|
+
Assert.That(AssetDatabase.GetAssetPath(component.referenceAsset), Is.EqualTo(TempReferenceAssetPath));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
[Test]
|
|
292
|
+
public void ObjectSetProperty_RejectsUnknownObjectReference()
|
|
293
|
+
{
|
|
294
|
+
var go = new GameObject("ReferenceCarrier");
|
|
295
|
+
go.AddComponent<ReferenceComponent>();
|
|
296
|
+
|
|
297
|
+
var response = _router.Dispatch(
|
|
298
|
+
"object/set-property",
|
|
299
|
+
1,
|
|
300
|
+
"{\"instanceId\":" + go.GetInstanceID() + ",\"component\":\"ReferenceComponent\",\"property\":\"referenceAsset\",\"value\":{\"path\":\"Assets/Missing.asset\"}}"
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
Assert.That(response.error, Is.Not.Null);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
[Test]
|
|
307
|
+
public void AssetWriteBatch_UpdatesMultipleFieldsIncludingObjectReference()
|
|
308
|
+
{
|
|
309
|
+
var reference = ScriptableObject.CreateInstance<SearchRootAsset>();
|
|
310
|
+
reference.name = "ReferenceAsset";
|
|
311
|
+
AssetDatabase.CreateAsset(reference, TempReferenceAssetPath);
|
|
312
|
+
|
|
313
|
+
var asset = ScriptableObject.CreateInstance<BatchWritableAsset>();
|
|
314
|
+
asset.maxPlayers = 2;
|
|
315
|
+
asset.spawnDelay = 5f;
|
|
316
|
+
AssetDatabase.CreateAsset(asset, TempAssetPath);
|
|
317
|
+
AssetDatabase.SaveAssets();
|
|
318
|
+
|
|
319
|
+
var response = _router.Dispatch(
|
|
320
|
+
"asset/write-batch",
|
|
321
|
+
1,
|
|
322
|
+
"{\"path\":\"" + TempAssetPath + "\",\"values\":{\"maxPlayers\":8,\"spawnDelay\":1.5,\"referenceAsset\":{\"path\":\"" + TempReferenceAssetPath + "\"}}}"
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
Assert.That(response.error, Is.Null);
|
|
326
|
+
|
|
327
|
+
var reloaded = AssetDatabase.LoadAssetAtPath<BatchWritableAsset>(TempAssetPath);
|
|
328
|
+
Assert.That(reloaded.maxPlayers, Is.EqualTo(8));
|
|
329
|
+
Assert.That(reloaded.spawnDelay, Is.EqualTo(1.5f).Within(0.001f));
|
|
330
|
+
Assert.That(AssetDatabase.GetAssetPath(reloaded.referenceAsset), Is.EqualTo(TempReferenceAssetPath));
|
|
331
|
+
}
|
|
332
|
+
|
|
249
333
|
[Test]
|
|
250
334
|
public void FileController_WritePatchRead_AndRejectsPathTraversal()
|
|
251
335
|
{
|
|
@@ -411,6 +495,15 @@ namespace UCP.Bridge.Tests
|
|
|
411
495
|
}
|
|
412
496
|
}
|
|
413
497
|
|
|
498
|
+
private static void DeleteTempReferenceAsset()
|
|
499
|
+
{
|
|
500
|
+
if (AssetDatabase.LoadMainAssetAtPath(TempReferenceAssetPath) != null)
|
|
501
|
+
{
|
|
502
|
+
AssetDatabase.DeleteAsset(TempReferenceAssetPath);
|
|
503
|
+
AssetDatabase.SaveAssets();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
414
507
|
private static void DeleteTempPrefab()
|
|
415
508
|
{
|
|
416
509
|
if (AssetDatabase.LoadMainAssetAtPath(TempPrefabPath) != null)
|
|
@@ -481,5 +574,17 @@ namespace UCP.Bridge.Tests
|
|
|
481
574
|
private sealed class SearchNestedAsset : ScriptableObject
|
|
482
575
|
{
|
|
483
576
|
}
|
|
577
|
+
|
|
578
|
+
private sealed class BatchWritableAsset : ScriptableObject
|
|
579
|
+
{
|
|
580
|
+
public int maxPlayers;
|
|
581
|
+
public float spawnDelay;
|
|
582
|
+
public SearchRootAsset referenceAsset;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private sealed class ReferenceComponent : MonoBehaviour
|
|
586
|
+
{
|
|
587
|
+
public SearchRootAsset referenceAsset;
|
|
588
|
+
}
|
|
484
589
|
}
|
|
485
590
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "com.ucp.bridge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"url": "https://github.com/mflRevan/unity-control-protocol.git"
|
|
22
22
|
},
|
|
23
23
|
"type": "tool",
|
|
24
|
-
"documentationUrl": "https://github.com/mflRevan/unity-control-protocol/blob/main/
|
|
24
|
+
"documentationUrl": "https://github.com/mflRevan/unity-control-protocol/blob/main/README.md",
|
|
25
25
|
"changelogUrl": "https://github.com/mflRevan/unity-control-protocol/blob/main/unity-package/com.ucp.bridge/CHANGELOG.md",
|
|
26
26
|
"dependencies": {}
|
|
27
27
|
}
|