@mflrevan/ucp 0.2.0 → 0.2.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 (66) hide show
  1. package/README.md +3 -3
  2. package/bridge/com.ucp.bridge/CHANGELOG.md +56 -0
  3. package/bridge/com.ucp.bridge/CHANGELOG.md.meta +7 -0
  4. package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +573 -0
  5. package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs.meta +2 -0
  6. package/bridge/com.ucp.bridge/Editor/Bridge.meta +8 -0
  7. package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +499 -0
  8. package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs.meta +2 -0
  9. package/bridge/com.ucp.bridge/Editor/Controllers/BuildController.cs +230 -0
  10. package/bridge/com.ucp.bridge/Editor/Controllers/BuildController.cs.meta +2 -0
  11. package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +26 -0
  12. package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs.meta +2 -0
  13. package/bridge/com.ucp.bridge/Editor/Controllers/EditorSettingsController.cs +435 -0
  14. package/bridge/com.ucp.bridge/Editor/Controllers/EditorSettingsController.cs.meta +2 -0
  15. package/bridge/com.ucp.bridge/Editor/Controllers/FileController.cs +130 -0
  16. package/bridge/com.ucp.bridge/Editor/Controllers/FileController.cs.meta +2 -0
  17. package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +319 -0
  18. package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs.meta +2 -0
  19. package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +291 -0
  20. package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs.meta +2 -0
  21. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +295 -0
  22. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs.meta +2 -0
  23. package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +38 -0
  24. package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs.meta +2 -0
  25. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +242 -0
  26. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs.meta +2 -0
  27. package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +551 -0
  28. package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs.meta +2 -0
  29. package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +70 -0
  30. package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs.meta +2 -0
  31. package/bridge/com.ucp.bridge/Editor/Controllers/ScreenshotController.cs +125 -0
  32. package/bridge/com.ucp.bridge/Editor/Controllers/ScreenshotController.cs.meta +2 -0
  33. package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs +104 -0
  34. package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs.meta +2 -0
  35. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +227 -0
  36. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs.meta +2 -0
  37. package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +180 -0
  38. package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs.meta +2 -0
  39. package/bridge/com.ucp.bridge/Editor/Controllers/VcsController.cs +611 -0
  40. package/bridge/com.ucp.bridge/Editor/Controllers/VcsController.cs.meta +2 -0
  41. package/bridge/com.ucp.bridge/Editor/Controllers.meta +8 -0
  42. package/bridge/com.ucp.bridge/Editor/Protocol/CommandRouter.cs +45 -0
  43. package/bridge/com.ucp.bridge/Editor/Protocol/CommandRouter.cs.meta +2 -0
  44. package/bridge/com.ucp.bridge/Editor/Protocol/MessageTypes.cs +80 -0
  45. package/bridge/com.ucp.bridge/Editor/Protocol/MessageTypes.cs.meta +2 -0
  46. package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +358 -0
  47. package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs.meta +2 -0
  48. package/bridge/com.ucp.bridge/Editor/Protocol.meta +8 -0
  49. package/bridge/com.ucp.bridge/Editor/Scripts/IUCPScript.cs +37 -0
  50. package/bridge/com.ucp.bridge/Editor/Scripts/IUCPScript.cs.meta +2 -0
  51. package/bridge/com.ucp.bridge/Editor/Scripts.meta +8 -0
  52. package/bridge/com.ucp.bridge/Editor/UCP.Bridge.Editor.asmdef +16 -0
  53. package/bridge/com.ucp.bridge/Editor/UCP.Bridge.Editor.asmdef.meta +7 -0
  54. package/bridge/com.ucp.bridge/Editor.meta +8 -0
  55. package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef +14 -0
  56. package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef.meta +7 -0
  57. package/bridge/com.ucp.bridge/Runtime.meta +8 -0
  58. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +194 -0
  59. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs.meta +2 -0
  60. package/bridge/com.ucp.bridge/Tests/Editor/UCP.Bridge.Editor.Tests.asmdef +12 -0
  61. package/bridge/com.ucp.bridge/Tests/Editor/UCP.Bridge.Editor.Tests.asmdef.meta +7 -0
  62. package/bridge/com.ucp.bridge/Tests/Editor.meta +8 -0
  63. package/bridge/com.ucp.bridge/Tests.meta +8 -0
  64. package/bridge/com.ucp.bridge/package.json +27 -0
  65. package/bridge/com.ucp.bridge/package.json.meta +7 -0
  66. package/package.json +1 -1
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @mflrevan/ucp
2
2
 
3
- Version `0.2.0` of the Unity Control Protocol CLI.
3
+ Version `0.2.3` of the Unity Control Protocol CLI.
4
4
 
5
- This package installs the `ucp` command and downloads the matching published binary for your platform during `postinstall`.
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
 
7
7
  ## Install
8
8
 
@@ -47,7 +47,7 @@ Or add this to `Packages/manifest.json`:
47
47
  ```json
48
48
  {
49
49
  "dependencies": {
50
- "com.ucp.bridge": "https://github.com/mflRevan/unity-control-protocol.git?path=unity-package/com.ucp.bridge"
50
+ "com.ucp.bridge": "https://github.com/mflRevan/unity-control-protocol.git?path=unity-package/com.ucp.bridge#v0.2.3"
51
51
  }
52
52
  }
53
53
  ```
@@ -0,0 +1,56 @@
1
+ # Changelog
2
+
3
+ ## [0.2.3] - 2026-03-12
4
+
5
+ ### Fixed
6
+
7
+ - Fixed the release packaging pipeline so metadata validation no longer depends on optional website-only files
8
+
9
+ ## [0.2.2] - 2026-03-12
10
+
11
+ ### Changed
12
+
13
+ - The bridge is now intended to be consumed through CLI-managed local mounts by default, with tracked manifest installation remaining an explicit opt-in path
14
+
15
+ ## [0.2.1] - 2026-03-12
16
+
17
+ ### Added
18
+
19
+ - `LogsController` buffered history support for `logs/tail`, `logs/search`, and `logs/get`
20
+ - EditMode smoke coverage for buffered log truncation, regex filtering, and id-window filtering
21
+
22
+ ### Changed
23
+
24
+ - Snapshot responses remain shallow by default and log-heavy reads are now designed for summary-first inspection
25
+
26
+ ### Fixed
27
+
28
+ - EditMode test duration reporting now uses the editor uptime clock to avoid negative durations
29
+
30
+ ## [0.2.0] - 2026-03-12
31
+
32
+ ### Added
33
+
34
+ - Asset controller for asset search, inspection, field reads, writes, and ScriptableObject creation
35
+ - Property and hierarchy controllers for GameObject, component, and hierarchy manipulation
36
+ - Material controller for shader properties and keywords
37
+ - Prefab controller for status, overrides, apply, revert, unpack, and prefab creation
38
+ - Editor settings controller for player, quality, physics, lighting, tags, and layers
39
+ - Build controller for targets, scenes, scripting defines, and build execution
40
+
41
+ ## [0.1.0] - 2026-03-09
42
+
43
+ ### Added
44
+
45
+ - Initial WebSocket bridge server
46
+ - Play/stop/pause control
47
+ - Compilation trigger
48
+ - Scene management (list, load, active)
49
+ - State snapshots
50
+ - Screenshot capture (Game view)
51
+ - Console log streaming
52
+ - Test runner integration
53
+ - File read/write/patch operations
54
+ - JSON-RPC 2.0 protocol
55
+ - Lock file discovery mechanism
56
+ - Per-session token authentication
@@ -0,0 +1,7 @@
1
+ fileFormatVersion: 2
2
+ guid: 40d4771ee6083664b8ca936ef74671f9
3
+ TextScriptImporter:
4
+ externalObjects: {}
5
+ userData:
6
+ assetBundleName:
7
+ assetBundleVariant:
@@ -0,0 +1,573 @@
1
+ using System;
2
+ using System.Collections.Concurrent;
3
+ using System.Collections.Generic;
4
+ using System.IO;
5
+ using System.Net;
6
+ using System.Net.Sockets;
7
+ using System.Net.WebSockets;
8
+ using System.Security.Cryptography;
9
+ using System.Text;
10
+ using System.Text.RegularExpressions;
11
+ using System.Threading;
12
+ using System.Threading.Tasks;
13
+ using UnityEditor;
14
+ using UnityEngine;
15
+
16
+ namespace UCP.Bridge
17
+ {
18
+ /// <summary>
19
+ /// WebSocket server that runs inside the Unity Editor.
20
+ /// Uses raw TcpListener + manual WebSocket upgrade for maximum compatibility.
21
+ /// Starts automatically via [InitializeOnLoad] and handles JSON-RPC commands.
22
+ /// </summary>
23
+ [InitializeOnLoad]
24
+ public static class BridgeServer
25
+ {
26
+ private const int DefaultPort = 21342;
27
+ private const int MaxPort = 21352;
28
+ private const int MaxConnections = 4;
29
+ private const string ProtocolVersion = "0.2.3";
30
+
31
+ private static TcpListener s_listener;
32
+ private static CancellationTokenSource s_cts;
33
+ private static readonly List<WebSocket> s_clients = new();
34
+ private static readonly object s_clientLock = new();
35
+ private static int s_port;
36
+ private static string s_token;
37
+ private static bool s_running;
38
+
39
+ // Main-thread action queue
40
+ private static readonly ConcurrentQueue<Action> s_mainThreadQueue = new();
41
+
42
+ // Command router
43
+ private static readonly CommandRouter s_router = new();
44
+
45
+ // Log subscribers
46
+ private static readonly HashSet<WebSocket> s_logSubscribers = new();
47
+
48
+ static BridgeServer()
49
+ {
50
+ // Use delayCall + update fallback to ensure reliable startup after domain reload
51
+ EditorApplication.delayCall += Initialize;
52
+ EditorApplication.update += EnsureRunning;
53
+ }
54
+
55
+ private static void EnsureRunning()
56
+ {
57
+ if (!s_running)
58
+ {
59
+ EditorApplication.update -= EnsureRunning;
60
+ Initialize();
61
+ }
62
+ else
63
+ {
64
+ EditorApplication.update -= EnsureRunning;
65
+ }
66
+ }
67
+
68
+ private static void Initialize()
69
+ {
70
+ if (s_running) return;
71
+
72
+ try
73
+ {
74
+ RegisterHandlers();
75
+
76
+ EditorApplication.update += PumpMainThread;
77
+ EditorApplication.quitting += Shutdown;
78
+ AssemblyReloadEvents.beforeAssemblyReload += Shutdown;
79
+ Application.logMessageReceived += OnLogMessage;
80
+
81
+ s_token = Guid.NewGuid().ToString("N").Substring(0, 16);
82
+ StartServer();
83
+ }
84
+ catch (Exception ex)
85
+ {
86
+ Debug.LogError($"[UCP] Failed to initialize bridge: {ex}");
87
+ }
88
+ }
89
+
90
+ private static void RegisterHandlers()
91
+ {
92
+ // Handshake
93
+ s_router.Register("handshake", (paramsJson) =>
94
+ {
95
+ return new
96
+ {
97
+ serverVersion = ProtocolVersion,
98
+ protocolVersion = ProtocolVersion,
99
+ unityVersion = Application.unityVersion,
100
+ projectName = Application.productName,
101
+ projectPath = Path.GetDirectoryName(Application.dataPath)
102
+ };
103
+ });
104
+
105
+ // Play mode
106
+ PlayModeController.Register(s_router);
107
+
108
+ // Compilation
109
+ CompilationController.Register(s_router);
110
+
111
+ // Scenes
112
+ SceneController.Register(s_router);
113
+
114
+ // Snapshots
115
+ SnapshotController.Register(s_router);
116
+
117
+ // Screenshots
118
+ ScreenshotController.Register(s_router);
119
+
120
+ // Logs
121
+ LogsController.Register(s_router);
122
+
123
+ // Tests
124
+ TestRunnerController.Register(s_router);
125
+
126
+ // Scripts (exec)
127
+ ScriptController.Register(s_router);
128
+
129
+ // Files
130
+ FileController.Register(s_router);
131
+
132
+ // Version Control (Unity VCS / Plastic SCM)
133
+ VcsController.Register(s_router);
134
+
135
+ // Object Properties
136
+ PropertyController.Register(s_router);
137
+
138
+ // Hierarchy Operations
139
+ HierarchyController.Register(s_router);
140
+
141
+ // Asset Management
142
+ AssetController.Register(s_router);
143
+
144
+ // Editor Settings (Player, Quality, Physics, Lighting, Tags/Layers)
145
+ EditorSettingsController.Register(s_router);
146
+
147
+ // Material Properties
148
+ MaterialController.Register(s_router);
149
+
150
+ // Prefab Operations
151
+ PrefabController.Register(s_router);
152
+
153
+ // Build Pipeline
154
+ BuildController.Register(s_router);
155
+ }
156
+
157
+ private static void StartServer()
158
+ {
159
+ s_cts = new CancellationTokenSource();
160
+
161
+ for (int port = DefaultPort; port <= MaxPort; port++)
162
+ {
163
+ try
164
+ {
165
+ s_listener = new TcpListener(IPAddress.Loopback, port);
166
+ s_listener.Start();
167
+ s_port = port;
168
+ s_running = true;
169
+ Debug.Log($"[UCP] Bridge server started on port {port}");
170
+ break;
171
+ }
172
+ catch (Exception)
173
+ {
174
+ s_listener?.Stop();
175
+ s_listener = null;
176
+ }
177
+ }
178
+
179
+ if (!s_running)
180
+ {
181
+ Debug.LogError("[UCP] Failed to start bridge server — all ports in use");
182
+ return;
183
+ }
184
+
185
+ WriteLockFile();
186
+ Task.Run(() => AcceptLoop(s_cts.Token));
187
+ }
188
+
189
+ private static async Task AcceptLoop(CancellationToken ct)
190
+ {
191
+ while (!ct.IsCancellationRequested && s_running)
192
+ {
193
+ try
194
+ {
195
+ var tcp = await s_listener.AcceptTcpClientAsync();
196
+ var stream = tcp.GetStream();
197
+
198
+ // Read HTTP upgrade request (may arrive in multiple segments)
199
+ var requestBuilder = new StringBuilder();
200
+ var buffer = new byte[4096];
201
+ do
202
+ {
203
+ int read = await stream.ReadAsync(buffer, 0, buffer.Length, ct);
204
+ if (read == 0) { tcp.Close(); continue; }
205
+ requestBuilder.Append(Encoding.UTF8.GetString(buffer, 0, read));
206
+ }
207
+ while (!requestBuilder.ToString().Contains("\r\n\r\n") && stream.DataAvailable);
208
+
209
+ var request = requestBuilder.ToString();
210
+
211
+ // Check if it's a WebSocket upgrade
212
+ if (!request.Contains("Upgrade: websocket", StringComparison.OrdinalIgnoreCase))
213
+ {
214
+ var resp = "HTTP/1.1 426 Upgrade Required\r\nContent-Length: 0\r\n\r\n";
215
+ var respBytes = Encoding.UTF8.GetBytes(resp);
216
+ await stream.WriteAsync(respBytes, 0, respBytes.Length, ct);
217
+ tcp.Close();
218
+ continue;
219
+ }
220
+
221
+ // Extract Sec-WebSocket-Key
222
+ var keyMatch = Regex.Match(request, @"Sec-WebSocket-Key:\s*(\S+)",
223
+ RegexOptions.IgnoreCase);
224
+ if (!keyMatch.Success)
225
+ {
226
+ Debug.LogError("[UCP] No Sec-WebSocket-Key found in request");
227
+ tcp.Close();
228
+ continue;
229
+ }
230
+
231
+ var wsKey = keyMatch.Groups[1].Value.Trim();
232
+ var acceptKey = ComputeWebSocketAcceptKey(wsKey);
233
+
234
+ // Send upgrade response
235
+ var upgradeResponse =
236
+ "HTTP/1.1 101 Switching Protocols\r\n" +
237
+ "Upgrade: websocket\r\n" +
238
+ "Connection: Upgrade\r\n" +
239
+ $"Sec-WebSocket-Accept: {acceptKey}\r\n\r\n";
240
+ var upgradeBytes = Encoding.UTF8.GetBytes(upgradeResponse);
241
+ await stream.WriteAsync(upgradeBytes, 0, upgradeBytes.Length, ct);
242
+
243
+ // Create WebSocket from stream
244
+ var ws = WebSocket.CreateFromStream(stream, true, null, TimeSpan.FromSeconds(30));
245
+
246
+ lock (s_clientLock)
247
+ {
248
+ if (s_clients.Count >= MaxConnections)
249
+ {
250
+ _ = ws.CloseAsync(WebSocketCloseStatus.PolicyViolation,
251
+ "Max connections reached", CancellationToken.None);
252
+ continue;
253
+ }
254
+ s_clients.Add(ws);
255
+ }
256
+
257
+ _ = Task.Run(() => HandleClient(ws, ct));
258
+ }
259
+ catch (ObjectDisposedException) { break; }
260
+ catch (SocketException) { break; }
261
+ catch (Exception ex)
262
+ {
263
+ if (!ct.IsCancellationRequested)
264
+ Debug.LogError($"[UCP] Accept error: {ex.Message}");
265
+ }
266
+ }
267
+ }
268
+
269
+ private static string ComputeWebSocketAcceptKey(string key)
270
+ {
271
+ const string magic = "258EAFA5-E914-47DA-95CA-5AB5DC85B11B";
272
+ var combined = key + magic;
273
+ var hash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(combined));
274
+ return Convert.ToBase64String(hash);
275
+ }
276
+
277
+ private static async Task HandleClient(WebSocket ws, CancellationToken ct)
278
+ {
279
+ var buffer = new byte[64 * 1024]; // 64KB buffer
280
+
281
+ try
282
+ {
283
+ while (ws.State == WebSocketState.Open && !ct.IsCancellationRequested)
284
+ {
285
+ var segment = new ArraySegment<byte>(buffer);
286
+ WebSocketReceiveResult received;
287
+ var messageBuilder = new StringBuilder();
288
+
289
+ do
290
+ {
291
+ received = await ws.ReceiveAsync(segment, ct);
292
+ if (received.MessageType == WebSocketMessageType.Close)
293
+ {
294
+ await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
295
+ return;
296
+ }
297
+ messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, received.Count));
298
+ }
299
+ while (!received.EndOfMessage);
300
+
301
+ var message = messageBuilder.ToString();
302
+ ProcessMessage(ws, message);
303
+ }
304
+ }
305
+ catch (OperationCanceledException) { }
306
+ catch (WebSocketException) { }
307
+ catch (Exception ex)
308
+ {
309
+ Debug.LogError($"[UCP] Client error: {ex.Message}");
310
+ }
311
+ finally
312
+ {
313
+ lock (s_clientLock)
314
+ {
315
+ s_clients.Remove(ws);
316
+ s_logSubscribers.Remove(ws);
317
+ }
318
+ }
319
+ }
320
+
321
+ private static void ProcessMessage(WebSocket ws, string message)
322
+ {
323
+ // Parse JSON-RPC request
324
+ long id = 0;
325
+ string method = null;
326
+ string paramsJson = "{}";
327
+
328
+ try
329
+ {
330
+ // Minimal JSON parsing using Unity's JsonUtility won't work well for dynamic JSON.
331
+ // Use a simple manual parse for the top-level fields.
332
+ var json = MiniJson.Deserialize(message) as Dictionary<string, object>;
333
+ if (json == null)
334
+ {
335
+ SendError(ws, 0, ErrorCodes.ParseError, "Invalid JSON");
336
+ return;
337
+ }
338
+
339
+ if (json.TryGetValue("id", out var idVal))
340
+ id = Convert.ToInt64(idVal);
341
+
342
+ if (json.TryGetValue("method", out var methodVal))
343
+ method = methodVal as string;
344
+
345
+ if (json.TryGetValue("params", out var paramsVal))
346
+ paramsJson = MiniJson.Serialize(paramsVal);
347
+ }
348
+ catch (Exception ex)
349
+ {
350
+ SendError(ws, 0, ErrorCodes.ParseError, $"Parse error: {ex.Message}");
351
+ return;
352
+ }
353
+
354
+ if (string.IsNullOrEmpty(method))
355
+ {
356
+ SendError(ws, id, ErrorCodes.InvalidRequest, "Missing 'method' field");
357
+ return;
358
+ }
359
+
360
+ // Handle log subscription specially
361
+ if (method == "logs/subscribe")
362
+ {
363
+ lock (s_clientLock) { s_logSubscribers.Add(ws); }
364
+ }
365
+ else if (method == "logs/unsubscribe")
366
+ {
367
+ lock (s_clientLock) { s_logSubscribers.Remove(ws); }
368
+ }
369
+
370
+ // Dispatch on main thread
371
+ var capturedId = id;
372
+ var capturedMethod = method;
373
+ var capturedParams = paramsJson;
374
+ var capturedWs = ws;
375
+
376
+ s_mainThreadQueue.Enqueue(() =>
377
+ {
378
+ var response = s_router.Dispatch(capturedMethod, capturedId, capturedParams);
379
+ SendResponse(capturedWs, response);
380
+ });
381
+ }
382
+
383
+ private static void SendResponse(WebSocket ws, JsonRpcResponse response)
384
+ {
385
+ try
386
+ {
387
+ var json = MiniJson.Serialize(ResponseToDict(response));
388
+ var bytes = Encoding.UTF8.GetBytes(json);
389
+ _ = ws.SendAsync(new ArraySegment<byte>(bytes),
390
+ WebSocketMessageType.Text, true, CancellationToken.None);
391
+ }
392
+ catch (Exception ex)
393
+ {
394
+ Debug.LogError($"[UCP] Send error: {ex.Message}");
395
+ }
396
+ }
397
+
398
+ private static void SendError(WebSocket ws, long id, int code, string message)
399
+ {
400
+ SendResponse(ws, JsonRpcResponse.Error(id, code, message));
401
+ }
402
+
403
+ /// <summary>
404
+ /// Send a notification to log subscribers only.
405
+ /// </summary>
406
+ public static void SendNotification(string method, object data)
407
+ {
408
+ SendNotificationTo(method, data, false);
409
+ }
410
+
411
+ /// <summary>
412
+ /// Broadcast a notification to ALL connected clients.
413
+ /// Use for test results and other non-log notifications.
414
+ /// </summary>
415
+ public static void BroadcastNotification(string method, object data)
416
+ {
417
+ SendNotificationTo(method, data, true);
418
+ }
419
+
420
+ private static void SendNotificationTo(string method, object data, bool toAll)
421
+ {
422
+ var dict = new Dictionary<string, object>
423
+ {
424
+ ["jsonrpc"] = "2.0",
425
+ ["method"] = method,
426
+ ["params"] = data
427
+ };
428
+ var json = MiniJson.Serialize(dict);
429
+ var bytes = Encoding.UTF8.GetBytes(json);
430
+ var segment = new ArraySegment<byte>(bytes);
431
+
432
+ List<WebSocket> targets;
433
+ lock (s_clientLock)
434
+ {
435
+ targets = toAll
436
+ ? new List<WebSocket>(s_clients)
437
+ : new List<WebSocket>(s_logSubscribers);
438
+ }
439
+
440
+ foreach (var ws in targets)
441
+ {
442
+ try
443
+ {
444
+ if (ws.State == WebSocketState.Open)
445
+ {
446
+ _ = ws.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None);
447
+ }
448
+ }
449
+ catch { /* ignore send failures for notifications */ }
450
+ }
451
+ }
452
+
453
+ private static void OnLogMessage(string message, string stackTrace, LogType type)
454
+ {
455
+ // Don't forward our own log messages to avoid infinite recursion
456
+ if (message.StartsWith("[UCP]")) return;
457
+
458
+ SendNotification("log", LogsController.RecordLog(message, stackTrace, type));
459
+ }
460
+
461
+ private static void PumpMainThread()
462
+ {
463
+ int processed = 0;
464
+ while (s_mainThreadQueue.TryDequeue(out var action) && processed < 50)
465
+ {
466
+ try { action(); }
467
+ catch (Exception ex) { Debug.LogError($"[UCP] Main thread error: {ex}"); }
468
+ processed++;
469
+ }
470
+ }
471
+
472
+ private static void WriteLockFile()
473
+ {
474
+ try
475
+ {
476
+ var projectPath = Path.GetDirectoryName(Application.dataPath);
477
+ var ucpDir = Path.Combine(projectPath, ".ucp");
478
+ Directory.CreateDirectory(ucpDir);
479
+
480
+ var lockData = new Dictionary<string, object>
481
+ {
482
+ ["pid"] = System.Diagnostics.Process.GetCurrentProcess().Id,
483
+ ["port"] = s_port,
484
+ ["protocolVersion"] = ProtocolVersion,
485
+ ["unityVersion"] = Application.unityVersion,
486
+ ["projectPath"] = projectPath,
487
+ ["startedAt"] = DateTime.UtcNow.ToString("o"),
488
+ ["token"] = s_token
489
+ };
490
+
491
+ File.WriteAllText(
492
+ Path.Combine(ucpDir, "bridge.lock"),
493
+ MiniJson.Serialize(lockData)
494
+ );
495
+ }
496
+ catch (Exception ex)
497
+ {
498
+ Debug.LogError($"[UCP] Failed to write lock file: {ex.Message}");
499
+ }
500
+ }
501
+
502
+ private static void CleanLockFile()
503
+ {
504
+ try
505
+ {
506
+ var projectPath = Path.GetDirectoryName(Application.dataPath);
507
+ var lockPath = Path.Combine(projectPath, ".ucp", "bridge.lock");
508
+ if (File.Exists(lockPath))
509
+ File.Delete(lockPath);
510
+ }
511
+ catch { }
512
+ }
513
+
514
+ private static void Shutdown()
515
+ {
516
+ if (!s_running) return;
517
+ s_running = false;
518
+
519
+ Debug.Log("[UCP] Bridge server shutting down");
520
+
521
+ s_cts?.Cancel();
522
+
523
+ // Stop listener first to release port immediately
524
+ try { s_listener?.Stop(); }
525
+ catch { }
526
+ s_listener = null;
527
+
528
+ lock (s_clientLock)
529
+ {
530
+ foreach (var ws in s_clients)
531
+ {
532
+ try { ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Shutting down", CancellationToken.None); }
533
+ catch { }
534
+ }
535
+ s_clients.Clear();
536
+ s_logSubscribers.Clear();
537
+ }
538
+
539
+ s_cts?.Dispose();
540
+ s_cts = null;
541
+
542
+ CleanLockFile();
543
+
544
+ EditorApplication.update -= PumpMainThread;
545
+ Application.logMessageReceived -= OnLogMessage;
546
+ }
547
+
548
+ private static Dictionary<string, object> ResponseToDict(JsonRpcResponse r)
549
+ {
550
+ var dict = new Dictionary<string, object>
551
+ {
552
+ ["jsonrpc"] = "2.0",
553
+ ["id"] = r.id
554
+ };
555
+
556
+ if (r.error != null)
557
+ {
558
+ dict["error"] = new Dictionary<string, object>
559
+ {
560
+ ["code"] = r.error.code,
561
+ ["message"] = r.error.message,
562
+ ["data"] = r.error.data
563
+ };
564
+ }
565
+ else
566
+ {
567
+ dict["result"] = r.result;
568
+ }
569
+
570
+ return dict;
571
+ }
572
+ }
573
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: b9a2c654603efd4489cde1f7449bdc4c
@@ -0,0 +1,8 @@
1
+ fileFormatVersion: 2
2
+ guid: f6c77174590222842b21f1d3b792cb65
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant: