@mflrevan/ucp 0.6.2 → 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 +48 -0
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +55 -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 +7 -7
- package/bridge/com.ucp.bridge/Editor/Controllers/RecordingController.cs +8 -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 +1 -1
- 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 +79 -4
- 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,53 @@
|
|
|
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
|
+
|
|
3
51
|
## [0.6.2] - 2026-08-31
|
|
4
52
|
|
|
5
53
|
### 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
|
|
|
@@ -145,6 +155,9 @@ namespace UCP.Bridge
|
|
|
145
155
|
// Lightweight video recording
|
|
146
156
|
RecordingController.Register(s_router);
|
|
147
157
|
|
|
158
|
+
// UI Toolkit authoring, inspection, linting, and capture
|
|
159
|
+
UiController.Register(s_router);
|
|
160
|
+
|
|
148
161
|
// Logs
|
|
149
162
|
LogsController.Register(s_router);
|
|
150
163
|
|
|
@@ -435,16 +448,25 @@ namespace UCP.Bridge
|
|
|
435
448
|
// Dispatch on main thread
|
|
436
449
|
s_mainThreadQueue.Enqueue(() =>
|
|
437
450
|
{
|
|
451
|
+
var logCursor = LogsController.GetLatestId();
|
|
438
452
|
var response = s_router.Dispatch(capturedMethod, capturedId, capturedParams);
|
|
439
|
-
SendResponse(capturedWs, response);
|
|
453
|
+
SendResponse(capturedWs, response, logCursor);
|
|
440
454
|
});
|
|
441
455
|
}
|
|
442
456
|
|
|
443
|
-
|
|
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)
|
|
444
466
|
{
|
|
445
467
|
try
|
|
446
468
|
{
|
|
447
|
-
var json = MiniJson.Serialize(ResponseToDict(response));
|
|
469
|
+
var json = MiniJson.Serialize(ResponseToDict(response, logCursor));
|
|
448
470
|
var bytes = Encoding.UTF8.GetBytes(json);
|
|
449
471
|
_ = ws.SendAsync(new ArraySegment<byte>(bytes),
|
|
450
472
|
WebSocketMessageType.Text, true, CancellationToken.None);
|
|
@@ -521,6 +543,8 @@ namespace UCP.Bridge
|
|
|
521
543
|
|
|
522
544
|
private static void PumpMainThread()
|
|
523
545
|
{
|
|
546
|
+
System.Threading.Volatile.Write(ref s_lastMainThreadTick, DateTime.UtcNow.Ticks);
|
|
547
|
+
System.Threading.Volatile.Write(ref s_compiling, EditorApplication.isCompiling ? 1 : 0);
|
|
524
548
|
int processed = 0;
|
|
525
549
|
while (s_mainThreadQueue.TryDequeue(out var action) && processed < 50)
|
|
526
550
|
{
|
|
@@ -574,6 +598,9 @@ namespace UCP.Bridge
|
|
|
574
598
|
|
|
575
599
|
private static void Shutdown()
|
|
576
600
|
{
|
|
601
|
+
#if UNITY_6000_0_OR_NEWER
|
|
602
|
+
UiOperationManager.Shutdown();
|
|
603
|
+
#endif
|
|
577
604
|
if (!s_running) return;
|
|
578
605
|
s_running = false;
|
|
579
606
|
|
|
@@ -608,7 +635,12 @@ namespace UCP.Bridge
|
|
|
608
635
|
Application.logMessageReceivedThreaded -= OnLogMessage;
|
|
609
636
|
}
|
|
610
637
|
|
|
611
|
-
|
|
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)
|
|
612
644
|
{
|
|
613
645
|
var dict = new Dictionary<string, object>
|
|
614
646
|
{
|
|
@@ -616,6 +648,23 @@ namespace UCP.Bridge
|
|
|
616
648
|
["id"] = r.id
|
|
617
649
|
};
|
|
618
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
|
+
|
|
619
668
|
if (r.error != null)
|
|
620
669
|
{
|
|
621
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++)
|
|
@@ -109,6 +109,30 @@ namespace UCP.Bridge
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/// <summary>
|
|
113
|
+
/// Error/exception and warning counts in the bridge history after <paramref name="afterId"/>.
|
|
114
|
+
/// Walks back from the newest entry and stops at the cursor, so the cost is proportional to
|
|
115
|
+
/// what the current request logged, not to the history size.
|
|
116
|
+
/// </summary>
|
|
117
|
+
public static void CountLevels(long afterId, out int errors, out int warnings)
|
|
118
|
+
{
|
|
119
|
+
errors = 0;
|
|
120
|
+
warnings = 0;
|
|
121
|
+
lock (s_historyLock)
|
|
122
|
+
{
|
|
123
|
+
for (var index = s_history.Count - 1; index >= 0; index--)
|
|
124
|
+
{
|
|
125
|
+
var entry = s_history[index];
|
|
126
|
+
if (entry.Id <= afterId)
|
|
127
|
+
break;
|
|
128
|
+
if (entry.Level == "error" || entry.Level == "exception")
|
|
129
|
+
errors++;
|
|
130
|
+
else if (entry.Level == "warning")
|
|
131
|
+
warnings++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
112
136
|
public static long GetLatestId()
|
|
113
137
|
{
|
|
114
138
|
SeedHistoryFromConsole();
|
|
@@ -132,6 +156,53 @@ namespace UCP.Bridge
|
|
|
132
156
|
}
|
|
133
157
|
}
|
|
134
158
|
|
|
159
|
+
/// <summary>
|
|
160
|
+
/// Status summary for the test-run console guard. Unity's test runner writes
|
|
161
|
+
/// "IgnoreFailingMessages:true" / "IgnoreFailingMessages:false" log lines whenever a test
|
|
162
|
+
/// toggles <c>LogAssert.ignoreFailingMessages</c>; everything logged inside such a window is
|
|
163
|
+
/// noise the test declared as expected, so it must not fail the guard. The markers
|
|
164
|
+
/// themselves are dropped as well.
|
|
165
|
+
/// </summary>
|
|
166
|
+
public static Dictionary<string, object> BuildTestGuardSummary(long afterId)
|
|
167
|
+
{
|
|
168
|
+
SeedHistoryFromConsole();
|
|
169
|
+
lock (s_historyLock)
|
|
170
|
+
{
|
|
171
|
+
var ordered = s_history
|
|
172
|
+
.Where(entry => entry.Id > afterId)
|
|
173
|
+
.OrderBy(entry => entry.Id)
|
|
174
|
+
.ToList();
|
|
175
|
+
|
|
176
|
+
return BuildStatusResult(ExcludeExpectedNoise(ordered, entry => entry.Message), afterId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
internal const string IgnoreFailingMessagesOnMarker = "IgnoreFailingMessages:true";
|
|
181
|
+
internal const string IgnoreFailingMessagesOffMarker = "IgnoreFailingMessages:false";
|
|
182
|
+
|
|
183
|
+
internal static List<T> ExcludeExpectedNoise<T>(IEnumerable<T> entries, Func<T, string> message)
|
|
184
|
+
{
|
|
185
|
+
var result = new List<T>();
|
|
186
|
+
var ignoring = false;
|
|
187
|
+
foreach (var entry in entries)
|
|
188
|
+
{
|
|
189
|
+
var text = (message(entry) ?? string.Empty).Trim();
|
|
190
|
+
if (text == IgnoreFailingMessagesOnMarker)
|
|
191
|
+
{
|
|
192
|
+
ignoring = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (text == IgnoreFailingMessagesOffMarker)
|
|
196
|
+
{
|
|
197
|
+
ignoring = false;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!ignoring)
|
|
201
|
+
result.Add(entry);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
135
206
|
private static object HandleTail(string paramsJson)
|
|
136
207
|
{
|
|
137
208
|
var query = ParseQuery(paramsJson, includePattern: false);
|
|
@@ -227,7 +227,7 @@ namespace UCP.Bridge
|
|
|
227
227
|
// Resolve by instanceId (scene object's renderer material)
|
|
228
228
|
if (p.TryGetValue("instanceId", out var idObj))
|
|
229
229
|
{
|
|
230
|
-
|
|
230
|
+
long instanceId = Convert.ToInt64(idObj);
|
|
231
231
|
var obj = UnityObjectCompat.ResolveByInstanceId(instanceId);
|
|
232
232
|
|
|
233
233
|
if (obj is Material directMat)
|
|
@@ -330,7 +330,7 @@ namespace UCP.Bridge
|
|
|
330
330
|
}
|
|
331
331
|
else if (texDict.TryGetValue("instanceId", out var tid))
|
|
332
332
|
{
|
|
333
|
-
var tex = UnityObjectCompat.ResolveByInstanceId<Texture>(Convert.
|
|
333
|
+
var tex = UnityObjectCompat.ResolveByInstanceId<Texture>(Convert.ToInt64(tid));
|
|
334
334
|
mat.SetTexture(propName, tex);
|
|
335
335
|
}
|
|
336
336
|
}
|
|
@@ -9,7 +9,7 @@ namespace UCP.Bridge
|
|
|
9
9
|
/// Shared GameObject resolution for the spatial/visual controllers.
|
|
10
10
|
///
|
|
11
11
|
/// A target may be addressed three ways, tried in priority order:
|
|
12
|
-
/// 1. instanceId (
|
|
12
|
+
/// 1. instanceId (64-bit integer) — canonical, survives nothing but a domain reload; preferred.
|
|
13
13
|
/// 2. path (string) — hierarchy path "Root/Child/Leaf" (leading '/' optional),
|
|
14
14
|
/// resolved across all loaded scenes; survives reloads.
|
|
15
15
|
/// 3. name (string) — first GameObject whose name matches; ambiguous under
|
|
@@ -32,7 +32,7 @@ namespace UCP.Bridge
|
|
|
32
32
|
|
|
33
33
|
if ((p.TryGetValue("instanceId", out var idObj) || p.TryGetValue("id", out idObj)) && idObj != null)
|
|
34
34
|
{
|
|
35
|
-
var id = Convert.
|
|
35
|
+
var id = Convert.ToInt64(idObj);
|
|
36
36
|
var byId = FindByInstanceId(id);
|
|
37
37
|
if (byId != null)
|
|
38
38
|
return byId;
|
|
@@ -60,7 +60,7 @@ namespace UCP.Bridge
|
|
|
60
60
|
throw new ArgumentException("Missing target: provide 'instanceId', 'path', or 'name'");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
internal static GameObject FindByInstanceId(
|
|
63
|
+
internal static GameObject FindByInstanceId(long instanceId)
|
|
64
64
|
{
|
|
65
65
|
var direct = UnityObjectCompat.ResolveByInstanceId<GameObject>(instanceId);
|
|
66
66
|
if (direct != null)
|
|
@@ -82,7 +82,7 @@ namespace UCP.Bridge
|
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
private static GameObject FindInHierarchyById(GameObject go,
|
|
85
|
+
private static GameObject FindInHierarchyById(GameObject go, long instanceId)
|
|
86
86
|
{
|
|
87
87
|
if (go.GetId() == instanceId)
|
|
88
88
|
return go;
|