@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.
Files changed (52) hide show
  1. package/README.md +1 -1
  2. package/bridge/com.ucp.bridge/CHANGELOG.md +110 -0
  3. package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +61 -6
  4. package/bridge/com.ucp.bridge/Editor/Bridge/EditorStateSummary.cs +165 -0
  5. package/bridge/com.ucp.bridge/Editor/Bridge/EditorStateSummary.cs.meta +11 -0
  6. package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs +22 -5
  7. package/bridge/com.ucp.bridge/Editor/Controllers/EditorController.cs +1 -0
  8. package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +10 -10
  9. package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +71 -0
  10. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +2 -2
  11. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectLocator.cs +4 -4
  12. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +3 -3
  13. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +6 -6
  14. package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +30 -14
  15. package/bridge/com.ucp.bridge/Editor/Controllers/RecordingController.cs +732 -0
  16. package/bridge/com.ucp.bridge/Editor/Controllers/RecordingController.cs.meta +11 -0
  17. package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +3 -3
  18. package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +3 -3
  19. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +5 -5
  20. package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +11 -2
  21. package/bridge/com.ucp.bridge/Editor/Controllers/TransformController.cs +1 -1
  22. package/bridge/com.ucp.bridge/Editor/Controllers/UiController.cs +70 -0
  23. package/bridge/com.ucp.bridge/Editor/Controllers/UiController.cs.meta +11 -0
  24. package/bridge/com.ucp.bridge/Editor/Controllers/ViewController.cs +1 -1
  25. package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +1 -1
  26. package/bridge/com.ucp.bridge/Editor/Ui/UiHost.cs +468 -0
  27. package/bridge/com.ucp.bridge/Editor/Ui/UiHost.cs.meta +11 -0
  28. package/bridge/com.ucp.bridge/Editor/Ui/UiLintService.cs +554 -0
  29. package/bridge/com.ucp.bridge/Editor/Ui/UiLintService.cs.meta +11 -0
  30. package/bridge/com.ucp.bridge/Editor/Ui/UiOperationManager.cs +870 -0
  31. package/bridge/com.ucp.bridge/Editor/Ui/UiOperationManager.cs.meta +11 -0
  32. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioApplier.cs +496 -0
  33. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioApplier.cs.meta +11 -0
  34. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioLoader.cs +854 -0
  35. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioLoader.cs.meta +11 -0
  36. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioModels.cs +374 -0
  37. package/bridge/com.ucp.bridge/Editor/Ui/UiScenarioModels.cs.meta +11 -0
  38. package/bridge/com.ucp.bridge/Editor/Ui/UiVisualTreeInspector.cs +690 -0
  39. package/bridge/com.ucp.bridge/Editor/Ui/UiVisualTreeInspector.cs.meta +11 -0
  40. package/bridge/com.ucp.bridge/{Runtime.meta → Editor/Ui.meta} +1 -1
  41. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +191 -3
  42. package/bridge/com.ucp.bridge/Tests/Editor/SpatialVisualControllerTests.cs +2 -2
  43. package/bridge/com.ucp.bridge/Tests/Editor/UiControllerTests.cs +547 -0
  44. package/bridge/com.ucp.bridge/Tests/Editor/UiControllerTests.cs.meta +11 -0
  45. package/bridge/com.ucp.bridge/Tests/Editor/UiLintServiceTests.cs +253 -0
  46. package/bridge/com.ucp.bridge/Tests/Editor/UiLintServiceTests.cs.meta +11 -0
  47. package/bridge/com.ucp.bridge/Tests/Editor/UiScenarioTests.cs +587 -0
  48. package/bridge/com.ucp.bridge/Tests/Editor/UiScenarioTests.cs.meta +11 -0
  49. package/bridge/com.ucp.bridge/package.json +1 -1
  50. package/package.json +1 -1
  51. package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef +0 -14
  52. 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.1` of the Unity Control Protocol CLI.
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.1";
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
- private static void SendResponse(WebSocket ws, JsonRpcResponse response)
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
- private static Dictionary<string, object> ResponseToDict(JsonRpcResponse r)
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
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 81e4ee8b39d34106904926156faa3bfe
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -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 int GetId(this Object obj)
16
+ public static long GetId(this Object obj)
10
17
  {
11
18
  #if UNITY_6000_5_OR_NEWER
12
- return unchecked((int)EntityId.ToULong(obj.GetEntityId()));
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(int instanceId)
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
- return EditorUtility.InstanceIDToObject(instanceId);
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>(int instanceId) where T : Object
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
- int parentId = Convert.ToInt32(parentObj);
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
- int instanceId = Convert.ToInt32(idObj);
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
- int instanceId = Convert.ToInt32(idObj);
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
- int parentId = Convert.ToInt32(parentObj);
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
- int srcId = Convert.ToInt32(srcObj);
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
- int parentId = Convert.ToInt32(parentObj);
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
- int instanceId = Convert.ToInt32(idObj);
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
- int instanceId = Convert.ToInt32(idObj);
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(int instanceId)
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, int instanceId)
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++)