@mflrevan/ucp 0.6.1 → 0.6.3
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/CHANGELOG.md +110 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +61 -6
- package/bridge/com.ucp.bridge/Editor/Bridge/EditorStateSummary.cs +165 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/EditorStateSummary.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +22 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/EditorController.cs +1 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +10 -10
- package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +71 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +4 -4
- package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +6 -6
- package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +30 -14
- package/bridge/com.ucp.bridge/Editor/Controllers/RecordingController.cs +732 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/RecordingController.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +3 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +5 -5
- package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +11 -2
- package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/UiController.cs +70 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/UiController.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +1 -1
- package/bridge/com.ucp.bridge/Editor/Ui/UiHost.cs +468 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiHost.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiLintService.cs +554 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiLintService.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiOperationManager.cs +870 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiOperationManager.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioApplier.cs +496 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioApplier.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioLoader.cs +854 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioLoader.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioModels.cs +374 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioModels.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiVisualTreeInspector.cs +690 -0
- package/bridge/com.ucp.bridge/Editor/Ui/UiVisualTreeInspector.cs.meta +11 -0
- package/bridge/com.ucp.bridge/{Runtime.meta → Editor/Ui.meta} +1 -1
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +191 -3
- package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +2 -2
- package/bridge/com.ucp.bridge/Tests/Editor/UiControllerTests.cs +547 -0
- package/bridge/com.ucp.bridge/Tests/Editor/UiControllerTests.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Tests/Editor/UiLintServiceTests.cs +253 -0
- package/bridge/com.ucp.bridge/Tests/Editor/UiLintServiceTests.cs.meta +11 -0
- package/bridge/com.ucp.bridge/Tests/Editor/UiScenarioTests.cs +587 -0
- package/bridge/com.ucp.bridge/Tests/Editor/UiScenarioTests.cs.meta +11 -0
- package/bridge/com.ucp.bridge/package.json +1 -1
- package/package.json +1 -1
- package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef +0 -14
- package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef.meta +0 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mflrevan/ucp
|
|
2
2
|
|
|
3
|
-
Version `0.6.
|
|
3
|
+
Version `0.6.3` 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
|
|
|
@@ -1,5 +1,115 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.3] - 2026-09-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Every response produced on the main thread carries an `editor` summary next to `result` or
|
|
8
|
+
`error` (`EditorStateSummary`): mode, active scene dirtiness, Console badge counts read
|
|
9
|
+
through `LogEntries.GetCountsByType`, entries logged since the request was dispatched, and
|
|
10
|
+
conditional flags for compile errors, compiling, importing, building, prefab stage, and
|
|
11
|
+
recording. The handshake, which is answered off the main thread, reports
|
|
12
|
+
`mainThreadTickAgeMs` and `compiling` so a client can recognise a blocked main thread or a
|
|
13
|
+
pending domain reload before sending a request. Added `editor/state`
|
|
14
|
+
to fetch the summary on its own.
|
|
15
|
+
- Added the `ui/list`, `ui/lint`, `ui/inspect`, `ui/screenshot`, `ui/check`, and `ui/status`
|
|
16
|
+
RPCs plus the `ui/result` completion notification for UI Toolkit authoring on Unity 6 and
|
|
17
|
+
newer. On older editors the methods are registered but return an explicit unsupported error.
|
|
18
|
+
- Lint runs the synchronous UXML/USS/TSS importers, reads their import logs and
|
|
19
|
+
`importedWithErrors`/`importedWithWarnings` flags, follows UI dependencies, and clone-probes
|
|
20
|
+
each `VisualTreeAsset` under a scoped log capture. Immutable package assets are inspected from
|
|
21
|
+
their existing import artifacts without a forced reimport.
|
|
22
|
+
- Render operations (`inspect`, `screenshot`, `check`) are queued and ticked on
|
|
23
|
+
`EditorApplication.update` in a transient `HideAndDontSave` utility window, wait for stable
|
|
24
|
+
finite geometry and stable pixels, capture through `InternalEditorUtility.CaptureEditorWindow`
|
|
25
|
+
with DPI compensation, write PNGs under `Library/UCP/UiCaptures`, and restore the previously
|
|
26
|
+
focused window. A 300-second overall ceiling and per-state settle timeouts bound every run;
|
|
27
|
+
`BridgeServer.Shutdown` fails in-flight operations with `editor_shutdown` before a reload.
|
|
28
|
+
- Scenario support: strict `.ucp-ui.json` parsing with unknown-field rejection, deep data
|
|
29
|
+
overlays, an allowlisted `set` block, `repeat` and virtualized `list-view` collections driven
|
|
30
|
+
by JSON pointers, and automatic rewriting of `DataBinding` paths to dictionary keys through
|
|
31
|
+
`Unity.Properties`.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Object ids are 64-bit end to end. `UnityObjectCompat.GetId()` returns a `long`
|
|
36
|
+
(`EntityId.ToULong` on 6000.5+, the instance id below that), `ResolveByInstanceId` takes a
|
|
37
|
+
`long`, and every controller reads `instanceId` parameters with `Convert.ToInt64`. On
|
|
38
|
+
6000.5 an `EntityId` carries a per-session upper word, so the previous `int` truncation
|
|
39
|
+
produced ids that could not be resolved back.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Fixed the `tests/run` console guard counting log lines emitted inside a test's
|
|
44
|
+
`LogAssert.ignoreFailingMessages` window; the `IgnoreFailingMessages:true/false` markers the
|
|
45
|
+
test runner writes now delimit an exclusion range.
|
|
46
|
+
- Fixed the edit-mode test assembly failing to compile on Unity 6000.5 (`GetInstanceID()` is
|
|
47
|
+
obsolete-as-error there); tests use the `EntityId`-aware `GetId()` helper throughout.
|
|
48
|
+
- Removed the empty `UCP.Bridge.Runtime` assembly definition that logged an "will not be
|
|
49
|
+
compiled, because it has no scripts associated with it" warning on every import (#4).
|
|
50
|
+
|
|
51
|
+
## [0.6.2] - 2026-08-31
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- Added native, scene-object-free `record/start`, `record/stop`, `record/status`, `record/arm`, and
|
|
56
|
+
`record/signal` RPCs for Game and Scene view video capture.
|
|
57
|
+
- Added aspect-preserving longest-edge sizing and explicit width/height canvases with letterboxing,
|
|
58
|
+
even-dimension normalization, configurable FPS/bitrate/duration, H.264/MP4 and VP8/WebM encoding,
|
|
59
|
+
and platform-aware format selection.
|
|
60
|
+
- Added play-enter, play-exit, bounded log-regex, and named-signal triggers. Armed play triggers use
|
|
61
|
+
`SessionState` to survive domain reloads, and `record/stop` also cancels a pending trigger.
|
|
62
|
+
- Added a `slowdown` parameter to `record/start` and `record/arm`. Frames are still captured at the
|
|
63
|
+
requested cadence in real time; only the encoder's declared frame rate is divided by the factor, so
|
|
64
|
+
playback is stretched without duplicating frames or re-encoding. `record/status` reports both
|
|
65
|
+
`slowdown` and the resulting `playbackFps`. Values outside 1-20 are rejected. This raises effective
|
|
66
|
+
temporal resolution for consumers that sample a clip at a fixed low rate rather than playing it.
|
|
67
|
+
- Documented that `record/start --view game` resolves `Camera.main`, not the Game view's composited
|
|
68
|
+
camera stack. Projects rendering through several enabled cameras record only the `MainCamera`-tagged
|
|
69
|
+
one, and camera depth does not change the selection; `--view scene` captures the Scene view camera,
|
|
70
|
+
which is unaffected by gameplay.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- Fixed `tests/run` counting an empty root suite as a passed test. When a filter matched nothing,
|
|
75
|
+
the run finished with a childless root whose result was collected as a leaf, so the summary
|
|
76
|
+
reported one passed test named after the project. Suite results are now skipped, so a run that
|
|
77
|
+
executed nothing reports a total of zero.
|
|
78
|
+
- Changed `tests/run` filtering from `Filter.testNames` to `Filter.groupNames`. `testNames` requires
|
|
79
|
+
an exact fully-qualified match, which silently selected nothing for a class or method name;
|
|
80
|
+
`groupNames` is matched as a regular expression against each test's full name, so partial names
|
|
81
|
+
select what a caller expects and fully-qualified names still match.
|
|
82
|
+
- Fixed `object/get-property` double-converting values that `SerializedPropertyToValue` had already
|
|
83
|
+
shaped for JSON. `GetPropertyValue` resolves a name through `SerializedObject.FindProperty` first
|
|
84
|
+
and returns a ready `List`/`Dictionary`; `ConvertToJson` then ran over that result, matched none
|
|
85
|
+
of its cases, and fell through to `value.ToString()`. A `Vector3` field came back as
|
|
86
|
+
``"System.Collections.Generic.List`1[System.Object]"`` with
|
|
87
|
+
``"type": "List`1"``. Every serialized Vector2/3/4, Quaternion, Color, Rect, Bounds and
|
|
88
|
+
object-reference field was affected, and float fields reported a `Double` type name.
|
|
89
|
+
`get-property` now takes its type name from the `SerializedProperty` so it agrees with
|
|
90
|
+
`get-fields`, converts only values read through the reflection fallback, and `ConvertToJson` is
|
|
91
|
+
idempotent for already-shaped `IList`/`IDictionary` values.
|
|
92
|
+
|
|
93
|
+
### Performance
|
|
94
|
+
|
|
95
|
+
- Frames render through a hidden reusable `RenderTexture` and reusable CPU readback texture into
|
|
96
|
+
Unity's native `MediaEncoder`; recording creates no scene objects or scripts and avoids per-frame
|
|
97
|
+
managed texture allocation.
|
|
98
|
+
|
|
99
|
+
### Reliability
|
|
100
|
+
|
|
101
|
+
- Added sibling `.partial` output and final rename, explicit overwrite handling, extension/format
|
|
102
|
+
validation, encoder and temporary-file cleanup on failure, detached safety deadlines, trigger wait
|
|
103
|
+
deadlines, dropped-frame accounting, and structured completed/failed status metadata.
|
|
104
|
+
- Active encoders finalize during assembly reload or editor shutdown. Play-boundary capture is
|
|
105
|
+
handled by the persisted `play-enter`/`play-exit` arm flow rather than attempting to retain a
|
|
106
|
+
native encoder across a domain reload.
|
|
107
|
+
|
|
108
|
+
### Tests
|
|
109
|
+
|
|
110
|
+
- Added editor smoke tests for recording RPC registration and idle status, aspect-preserving even
|
|
111
|
+
dimensions, signal matching, and cancellation of armed recordings.
|
|
112
|
+
|
|
3
113
|
## [0.4.1] - 2026-03-21
|
|
4
114
|
|
|
5
115
|
### 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.6.
|
|
29
|
+
private const string ProtocolVersion = "0.6.3";
|
|
30
30
|
|
|
31
31
|
private static TcpListener s_listener;
|
|
32
32
|
private static CancellationTokenSource s_cts;
|
|
@@ -45,6 +45,14 @@ namespace UCP.Bridge
|
|
|
45
45
|
|
|
46
46
|
// Main-thread action queue
|
|
47
47
|
private static readonly ConcurrentQueue<Action> s_mainThreadQueue = new();
|
|
48
|
+
// UTC ticks of the last EditorApplication.update we saw. Written by the main thread,
|
|
49
|
+
// read by the socket thread in the handshake so a client can tell "the editor's main
|
|
50
|
+
// thread is stalled" (modal dialog, synchronous import, domain reload) apart from "busy".
|
|
51
|
+
private static long s_lastMainThreadTick;
|
|
52
|
+
// EditorApplication.isCompiling as of the last pump, so the off-thread handshake can warn
|
|
53
|
+
// a client that a domain reload is imminent instead of letting its request race it.
|
|
54
|
+
private static int s_compiling;
|
|
55
|
+
private static bool s_summaryFailureLogged;
|
|
48
56
|
|
|
49
57
|
// Command router
|
|
50
58
|
private static readonly CommandRouter s_router = new();
|
|
@@ -120,7 +128,9 @@ namespace UCP.Bridge
|
|
|
120
128
|
protocolVersion = ProtocolVersion,
|
|
121
129
|
unityVersion = s_unityVersion,
|
|
122
130
|
projectName = s_projectName,
|
|
123
|
-
projectPath = s_projectPath
|
|
131
|
+
projectPath = s_projectPath,
|
|
132
|
+
mainThreadTickAgeMs = MainThreadTickAgeMs(),
|
|
133
|
+
compiling = System.Threading.Volatile.Read(ref s_compiling) == 1
|
|
124
134
|
};
|
|
125
135
|
});
|
|
126
136
|
|
|
@@ -142,6 +152,12 @@ namespace UCP.Bridge
|
|
|
142
152
|
// Screenshots
|
|
143
153
|
ScreenshotController.Register(s_router);
|
|
144
154
|
|
|
155
|
+
// Lightweight video recording
|
|
156
|
+
RecordingController.Register(s_router);
|
|
157
|
+
|
|
158
|
+
// UI Toolkit authoring, inspection, linting, and capture
|
|
159
|
+
UiController.Register(s_router);
|
|
160
|
+
|
|
145
161
|
// Logs
|
|
146
162
|
LogsController.Register(s_router);
|
|
147
163
|
|
|
@@ -432,16 +448,25 @@ namespace UCP.Bridge
|
|
|
432
448
|
// Dispatch on main thread
|
|
433
449
|
s_mainThreadQueue.Enqueue(() =>
|
|
434
450
|
{
|
|
451
|
+
var logCursor = LogsController.GetLatestId();
|
|
435
452
|
var response = s_router.Dispatch(capturedMethod, capturedId, capturedParams);
|
|
436
|
-
SendResponse(capturedWs, response);
|
|
453
|
+
SendResponse(capturedWs, response, logCursor);
|
|
437
454
|
});
|
|
438
455
|
}
|
|
439
456
|
|
|
440
|
-
|
|
457
|
+
/// <summary>Milliseconds since the main thread last pumped; -1 before the first pump.</summary>
|
|
458
|
+
internal static long MainThreadTickAgeMs()
|
|
459
|
+
{
|
|
460
|
+
var last = System.Threading.Volatile.Read(ref s_lastMainThreadTick);
|
|
461
|
+
if (last == 0) return -1;
|
|
462
|
+
return (DateTime.UtcNow.Ticks - last) / TimeSpan.TicksPerMillisecond;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private static void SendResponse(WebSocket ws, JsonRpcResponse response, long? logCursor = null)
|
|
441
466
|
{
|
|
442
467
|
try
|
|
443
468
|
{
|
|
444
|
-
var json = MiniJson.Serialize(ResponseToDict(response));
|
|
469
|
+
var json = MiniJson.Serialize(ResponseToDict(response, logCursor));
|
|
445
470
|
var bytes = Encoding.UTF8.GetBytes(json);
|
|
446
471
|
_ = ws.SendAsync(new ArraySegment<byte>(bytes),
|
|
447
472
|
WebSocketMessageType.Text, true, CancellationToken.None);
|
|
@@ -509,6 +534,7 @@ namespace UCP.Bridge
|
|
|
509
534
|
|
|
510
535
|
private static void OnLogMessage(string message, string stackTrace, LogType type)
|
|
511
536
|
{
|
|
537
|
+
RecordingController.NotifyLog(message);
|
|
512
538
|
// Don't forward our own log messages to avoid infinite recursion
|
|
513
539
|
if (message.StartsWith("[UCP]")) return;
|
|
514
540
|
|
|
@@ -517,6 +543,8 @@ namespace UCP.Bridge
|
|
|
517
543
|
|
|
518
544
|
private static void PumpMainThread()
|
|
519
545
|
{
|
|
546
|
+
System.Threading.Volatile.Write(ref s_lastMainThreadTick, DateTime.UtcNow.Ticks);
|
|
547
|
+
System.Threading.Volatile.Write(ref s_compiling, EditorApplication.isCompiling ? 1 : 0);
|
|
520
548
|
int processed = 0;
|
|
521
549
|
while (s_mainThreadQueue.TryDequeue(out var action) && processed < 50)
|
|
522
550
|
{
|
|
@@ -570,11 +598,16 @@ namespace UCP.Bridge
|
|
|
570
598
|
|
|
571
599
|
private static void Shutdown()
|
|
572
600
|
{
|
|
601
|
+
#if UNITY_6000_0_OR_NEWER
|
|
602
|
+
UiOperationManager.Shutdown();
|
|
603
|
+
#endif
|
|
573
604
|
if (!s_running) return;
|
|
574
605
|
s_running = false;
|
|
575
606
|
|
|
576
607
|
Debug.Log("[UCP] Bridge server shutting down");
|
|
577
608
|
|
|
609
|
+
RecordingController.Shutdown();
|
|
610
|
+
|
|
578
611
|
s_cts?.Cancel();
|
|
579
612
|
|
|
580
613
|
// Stop listener first to release port immediately
|
|
@@ -602,7 +635,12 @@ namespace UCP.Bridge
|
|
|
602
635
|
Application.logMessageReceivedThreaded -= OnLogMessage;
|
|
603
636
|
}
|
|
604
637
|
|
|
605
|
-
|
|
638
|
+
/// <param name="logCursor">
|
|
639
|
+
/// Present for responses produced on the main thread: the log id captured before dispatch.
|
|
640
|
+
/// The editor-state summary is attached as a sibling of result/error, and only then --
|
|
641
|
+
/// the handshake is answered off-thread where none of the editor APIs may be touched.
|
|
642
|
+
/// </param>
|
|
643
|
+
internal static Dictionary<string, object> ResponseToDict(JsonRpcResponse r, long? logCursor = null)
|
|
606
644
|
{
|
|
607
645
|
var dict = new Dictionary<string, object>
|
|
608
646
|
{
|
|
@@ -610,6 +648,23 @@ namespace UCP.Bridge
|
|
|
610
648
|
["id"] = r.id
|
|
611
649
|
};
|
|
612
650
|
|
|
651
|
+
if (logCursor.HasValue)
|
|
652
|
+
{
|
|
653
|
+
try
|
|
654
|
+
{
|
|
655
|
+
dict["editor"] = EditorStateSummary.Capture(logCursor.Value);
|
|
656
|
+
}
|
|
657
|
+
catch (Exception ex)
|
|
658
|
+
{
|
|
659
|
+
// Never let awareness break a response; note the failure once per domain.
|
|
660
|
+
if (!s_summaryFailureLogged)
|
|
661
|
+
{
|
|
662
|
+
s_summaryFailureLogged = true;
|
|
663
|
+
Debug.LogWarning($"[UCP] editor state summary unavailable: {ex.Message}");
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
613
668
|
if (r.error != null)
|
|
614
669
|
{
|
|
615
670
|
dict["error"] = new Dictionary<string, object>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Reflection;
|
|
4
|
+
using UnityEditor;
|
|
5
|
+
using UnityEditor.SceneManagement;
|
|
6
|
+
using UnityEngine;
|
|
7
|
+
using UnityEngine.SceneManagement;
|
|
8
|
+
|
|
9
|
+
namespace UCP.Bridge
|
|
10
|
+
{
|
|
11
|
+
/// <summary>
|
|
12
|
+
/// The compact editor state that rides on every main-thread response, so an agent is told
|
|
13
|
+
/// whether the console is red, whether the editor is playing or compiling, and whether the
|
|
14
|
+
/// scene is dirty without spending a request on it.
|
|
15
|
+
///
|
|
16
|
+
/// Budget: this runs once per response on the main thread, so it may only use O(1) editor
|
|
17
|
+
/// queries and cached reflection. It must never allocate more than a few small dictionaries,
|
|
18
|
+
/// never touch the asset database, and never throw -- a summary failure is swallowed by the
|
|
19
|
+
/// caller so it can never break a response.
|
|
20
|
+
/// </summary>
|
|
21
|
+
public static class EditorStateSummary
|
|
22
|
+
{
|
|
23
|
+
private delegate void GetCountsByType(ref int errors, ref int warnings, ref int logs);
|
|
24
|
+
|
|
25
|
+
private static GetCountsByType s_getCountsByType;
|
|
26
|
+
private static bool s_countsLookedUp;
|
|
27
|
+
|
|
28
|
+
/// <param name="logsSinceId">
|
|
29
|
+
/// The log history cursor captured before the request was dispatched; entries after it
|
|
30
|
+
/// were produced by this request and are reported as "new".
|
|
31
|
+
/// </param>
|
|
32
|
+
public static Dictionary<string, object> Capture(long logsSinceId)
|
|
33
|
+
{
|
|
34
|
+
var summary = new Dictionary<string, object>(8);
|
|
35
|
+
|
|
36
|
+
var playing = EditorApplication.isPlaying;
|
|
37
|
+
summary["mode"] = playing
|
|
38
|
+
? (EditorApplication.isPaused ? "paused" : "play")
|
|
39
|
+
: "edit";
|
|
40
|
+
if (EditorApplication.isPlayingOrWillChangePlaymode != playing)
|
|
41
|
+
summary["modeChanging"] = true;
|
|
42
|
+
if (EditorApplication.isCompiling)
|
|
43
|
+
summary["compiling"] = true;
|
|
44
|
+
if (EditorApplication.isUpdating)
|
|
45
|
+
summary["importing"] = true;
|
|
46
|
+
if (EditorUtility.scriptCompilationFailed)
|
|
47
|
+
summary["compileErrors"] = true;
|
|
48
|
+
if (BuildPipeline.isBuildingPlayer)
|
|
49
|
+
summary["building"] = true;
|
|
50
|
+
|
|
51
|
+
summary["scene"] = DescribeScenes();
|
|
52
|
+
|
|
53
|
+
var stage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
54
|
+
if (stage != null)
|
|
55
|
+
summary["prefabStage"] = stage.assetPath;
|
|
56
|
+
|
|
57
|
+
summary["console"] = DescribeConsole(logsSinceId);
|
|
58
|
+
|
|
59
|
+
var recording = RecordingController.StateForSummary();
|
|
60
|
+
if (recording != null)
|
|
61
|
+
summary["recording"] = recording;
|
|
62
|
+
|
|
63
|
+
return summary;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static Dictionary<string, object> DescribeScenes()
|
|
67
|
+
{
|
|
68
|
+
var active = SceneManager.GetActiveScene();
|
|
69
|
+
var scene = new Dictionary<string, object>(4)
|
|
70
|
+
{
|
|
71
|
+
["name"] = active.name,
|
|
72
|
+
["dirty"] = active.isDirty
|
|
73
|
+
};
|
|
74
|
+
if (string.IsNullOrEmpty(active.path))
|
|
75
|
+
scene["untitled"] = true;
|
|
76
|
+
|
|
77
|
+
var count = SceneManager.sceneCount;
|
|
78
|
+
if (count > 1)
|
|
79
|
+
{
|
|
80
|
+
var otherDirty = 0;
|
|
81
|
+
for (var index = 0; index < count; index++)
|
|
82
|
+
{
|
|
83
|
+
var candidate = SceneManager.GetSceneAt(index);
|
|
84
|
+
if (candidate != active && candidate.isDirty)
|
|
85
|
+
otherDirty++;
|
|
86
|
+
}
|
|
87
|
+
scene["loaded"] = count;
|
|
88
|
+
if (otherDirty > 0)
|
|
89
|
+
scene["otherDirty"] = otherDirty;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return scene;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private static Dictionary<string, object> DescribeConsole(long logsSinceId)
|
|
96
|
+
{
|
|
97
|
+
var console = new Dictionary<string, object>(4);
|
|
98
|
+
if (TryReadConsoleCounts(out var errors, out var warnings))
|
|
99
|
+
{
|
|
100
|
+
console["errors"] = errors;
|
|
101
|
+
console["warnings"] = warnings;
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
{
|
|
105
|
+
// Reflection into UnityEditor.LogEntries failed on this editor build; fall back to
|
|
106
|
+
// the bridge's own history, which at least covers everything since the bridge loaded.
|
|
107
|
+
LogsController.CountLevels(0, out errors, out warnings);
|
|
108
|
+
console["errors"] = errors;
|
|
109
|
+
console["warnings"] = warnings;
|
|
110
|
+
console["source"] = "bridge";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
LogsController.CountLevels(logsSinceId, out var newErrors, out var newWarnings);
|
|
114
|
+
if (newErrors > 0)
|
|
115
|
+
console["newErrors"] = newErrors;
|
|
116
|
+
if (newWarnings > 0)
|
|
117
|
+
console["newWarnings"] = newWarnings;
|
|
118
|
+
return console;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// <summary>
|
|
122
|
+
/// The counts the Console window's toolbar badges show, read through the same internal
|
|
123
|
+
/// API the window uses. Cached after the first lookup; the call itself is native and cheap.
|
|
124
|
+
/// </summary>
|
|
125
|
+
internal static bool TryReadConsoleCounts(out int errors, out int warnings)
|
|
126
|
+
{
|
|
127
|
+
errors = 0;
|
|
128
|
+
warnings = 0;
|
|
129
|
+
if (!s_countsLookedUp)
|
|
130
|
+
{
|
|
131
|
+
s_countsLookedUp = true;
|
|
132
|
+
try
|
|
133
|
+
{
|
|
134
|
+
var type = typeof(Editor).Assembly.GetType("UnityEditor.LogEntries");
|
|
135
|
+
var method = type?.GetMethod(
|
|
136
|
+
"GetCountsByType",
|
|
137
|
+
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
|
|
138
|
+
if (method != null)
|
|
139
|
+
{
|
|
140
|
+
s_getCountsByType = (GetCountsByType)Delegate.CreateDelegate(
|
|
141
|
+
typeof(GetCountsByType), method, false);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch
|
|
145
|
+
{
|
|
146
|
+
s_getCountsByType = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (s_getCountsByType == null)
|
|
151
|
+
return false;
|
|
152
|
+
|
|
153
|
+
try
|
|
154
|
+
{
|
|
155
|
+
var logs = 0;
|
|
156
|
+
s_getCountsByType(ref errors, ref warnings, ref logs);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
catch
|
|
160
|
+
{
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -4,17 +4,30 @@ using UnityEngine.SceneManagement;
|
|
|
4
4
|
|
|
5
5
|
namespace UCP.Bridge
|
|
6
6
|
{
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Object identity across the Unity 6 line. Up to 6000.4 an object is identified by a 32-bit
|
|
9
|
+
/// instance id; from 6000.5 it is a 64-bit <c>EntityId</c> whose upper word is a session
|
|
10
|
+
/// stamp and whose lower word is the classic id, and neither sign- nor zero-extending the
|
|
11
|
+
/// lower word resolves the object again. The wire protocol therefore carries ids as 64-bit
|
|
12
|
+
/// integers everywhere. On older editors the values are the same numbers they always were.
|
|
13
|
+
/// </summary>
|
|
7
14
|
internal static class UnityObjectCompat
|
|
8
15
|
{
|
|
9
|
-
public static
|
|
16
|
+
public static long GetId(this Object obj)
|
|
10
17
|
{
|
|
11
18
|
#if UNITY_6000_5_OR_NEWER
|
|
12
|
-
return unchecked((
|
|
19
|
+
return unchecked((long)EntityId.ToULong(obj.GetEntityId()));
|
|
13
20
|
#else
|
|
14
21
|
return obj.GetInstanceID();
|
|
15
22
|
#endif
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
/// <summary>Reads an id parameter from a parsed JSON payload (int, long, or numeric string).</summary>
|
|
26
|
+
public static long ReadId(object value)
|
|
27
|
+
{
|
|
28
|
+
return System.Convert.ToInt64(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
public static long GetSceneHandle(Scene scene)
|
|
19
32
|
{
|
|
20
33
|
#if UNITY_6000_5_OR_NEWER
|
|
@@ -27,16 +40,20 @@ namespace UCP.Bridge
|
|
|
27
40
|
#endif
|
|
28
41
|
}
|
|
29
42
|
|
|
30
|
-
public static Object ResolveByInstanceId(
|
|
43
|
+
public static Object ResolveByInstanceId(long instanceId)
|
|
31
44
|
{
|
|
32
45
|
#if UNITY_6000_5_OR_NEWER
|
|
33
46
|
return EditorUtility.EntityIdToObject(EntityId.FromULong(unchecked((ulong)instanceId)));
|
|
34
47
|
#else
|
|
35
|
-
|
|
48
|
+
// 32-bit editors cannot have handed out anything wider; treat an out-of-range id as
|
|
49
|
+
// "no such object" rather than truncating it into a different object's id.
|
|
50
|
+
if (instanceId < int.MinValue || instanceId > int.MaxValue)
|
|
51
|
+
return null;
|
|
52
|
+
return EditorUtility.InstanceIDToObject((int)instanceId);
|
|
36
53
|
#endif
|
|
37
54
|
}
|
|
38
55
|
|
|
39
|
-
public static T ResolveByInstanceId<T>(
|
|
56
|
+
public static T ResolveByInstanceId<T>(long instanceId) where T : Object
|
|
40
57
|
{
|
|
41
58
|
return ResolveByInstanceId(instanceId) as T;
|
|
42
59
|
}
|
|
@@ -7,6 +7,7 @@ namespace UCP.Bridge
|
|
|
7
7
|
public static void Register(CommandRouter router)
|
|
8
8
|
{
|
|
9
9
|
router.Register("editor/status", HandleStatus);
|
|
10
|
+
router.Register("editor/state", _ => EditorStateSummary.Capture(LogsController.GetLatestId()));
|
|
10
11
|
router.Register("editor/quit", HandleQuit);
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -44,7 +44,7 @@ namespace UCP.Bridge
|
|
|
44
44
|
// Optional parent
|
|
45
45
|
if (p != null && p.TryGetValue("parent", out var parentObj))
|
|
46
46
|
{
|
|
47
|
-
|
|
47
|
+
long parentId = Convert.ToInt64(parentObj);
|
|
48
48
|
var parent = FindGameObject(parentId);
|
|
49
49
|
go.transform.SetParent(parent.transform, false);
|
|
50
50
|
}
|
|
@@ -84,7 +84,7 @@ namespace UCP.Bridge
|
|
|
84
84
|
if (p == null || !p.TryGetValue("instanceId", out var idObj))
|
|
85
85
|
throw new ArgumentException("Missing 'instanceId' parameter");
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
long instanceId = Convert.ToInt64(idObj);
|
|
88
88
|
var go = FindGameObject(instanceId);
|
|
89
89
|
string name = go.name;
|
|
90
90
|
var scene = go.scene;
|
|
@@ -107,14 +107,14 @@ namespace UCP.Bridge
|
|
|
107
107
|
if (p == null || !p.TryGetValue("instanceId", out var idObj))
|
|
108
108
|
throw new ArgumentException("Missing 'instanceId' parameter");
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
long instanceId = Convert.ToInt64(idObj);
|
|
111
111
|
var go = FindGameObject(instanceId);
|
|
112
112
|
|
|
113
113
|
Undo.SetTransformParent(go.transform, null, "UCP Reparent");
|
|
114
114
|
|
|
115
115
|
if (p.TryGetValue("parent", out var parentObj) && parentObj != null)
|
|
116
116
|
{
|
|
117
|
-
|
|
117
|
+
long parentId = Convert.ToInt64(parentObj);
|
|
118
118
|
var parent = FindGameObject(parentId);
|
|
119
119
|
Undo.SetTransformParent(go.transform, parent.transform, "UCP Reparent");
|
|
120
120
|
}
|
|
@@ -163,7 +163,7 @@ namespace UCP.Bridge
|
|
|
163
163
|
// Instantiate from existing scene object (clone)
|
|
164
164
|
else if (p.TryGetValue("sourceId", out var srcObj))
|
|
165
165
|
{
|
|
166
|
-
|
|
166
|
+
long srcId = Convert.ToInt64(srcObj);
|
|
167
167
|
source = FindGameObject(srcId);
|
|
168
168
|
}
|
|
169
169
|
else
|
|
@@ -183,7 +183,7 @@ namespace UCP.Bridge
|
|
|
183
183
|
// Optional parent
|
|
184
184
|
if (p.TryGetValue("parent", out var parentObj) && parentObj != null)
|
|
185
185
|
{
|
|
186
|
-
|
|
186
|
+
long parentId = Convert.ToInt64(parentObj);
|
|
187
187
|
var parent = FindGameObject(parentId);
|
|
188
188
|
instance.transform.SetParent(parent.transform, false);
|
|
189
189
|
}
|
|
@@ -222,7 +222,7 @@ namespace UCP.Bridge
|
|
|
222
222
|
if (!p.TryGetValue("type", out var typeObj) || typeObj == null)
|
|
223
223
|
throw new ArgumentException("Missing 'type' parameter");
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
long instanceId = Convert.ToInt64(idObj);
|
|
226
226
|
var go = FindGameObject(instanceId);
|
|
227
227
|
string typeName = typeObj.ToString();
|
|
228
228
|
|
|
@@ -252,7 +252,7 @@ namespace UCP.Bridge
|
|
|
252
252
|
if (!p.TryGetValue("type", out var typeObj) || typeObj == null)
|
|
253
253
|
throw new ArgumentException("Missing 'type' parameter");
|
|
254
254
|
|
|
255
|
-
|
|
255
|
+
long instanceId = Convert.ToInt64(idObj);
|
|
256
256
|
var go = FindGameObject(instanceId);
|
|
257
257
|
string typeName = typeObj.ToString();
|
|
258
258
|
|
|
@@ -344,7 +344,7 @@ namespace UCP.Bridge
|
|
|
344
344
|
return null;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
private static GameObject FindGameObject(
|
|
347
|
+
private static GameObject FindGameObject(long instanceId)
|
|
348
348
|
{
|
|
349
349
|
var obj = UnityObjectCompat.ResolveByInstanceId<GameObject>(instanceId);
|
|
350
350
|
if (obj != null) return obj;
|
|
@@ -363,7 +363,7 @@ namespace UCP.Bridge
|
|
|
363
363
|
throw new ArgumentException($"GameObject not found: {instanceId}");
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
private static GameObject FindInHierarchy(GameObject go,
|
|
366
|
+
private static GameObject FindInHierarchy(GameObject go, long instanceId)
|
|
367
367
|
{
|
|
368
368
|
if (go.GetId() == instanceId) return go;
|
|
369
369
|
for (int i = 0; i < go.transform.childCount; i++)
|