@mflrevan/ucp 0.4.4 → 0.4.6

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 (84) hide show
  1. package/README.md +1 -1
  2. package/bridge/com.ucp.bridge/CHANGELOG.md +145 -0
  3. package/bridge/com.ucp.bridge/CHANGELOG.md.meta +7 -0
  4. package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +583 -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/Compatibility/UnityObjectCompat.cs +18 -0
  8. package/bridge/com.ucp.bridge/Editor/Compatibility/UnityObjectCompat.cs.meta +2 -0
  9. package/bridge/com.ucp.bridge/Editor/Compatibility.meta +8 -0
  10. package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +425 -0
  11. package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs.meta +2 -0
  12. package/bridge/com.ucp.bridge/Editor/Controllers/AssetImportSupport.cs +355 -0
  13. package/bridge/com.ucp.bridge/Editor/Controllers/AssetImportSupport.cs.meta +2 -0
  14. package/bridge/com.ucp.bridge/Editor/Controllers/BuildController.cs +233 -0
  15. package/bridge/com.ucp.bridge/Editor/Controllers/BuildController.cs.meta +2 -0
  16. package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +26 -0
  17. package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs.meta +2 -0
  18. package/bridge/com.ucp.bridge/Editor/Controllers/EditorController.cs +31 -0
  19. package/bridge/com.ucp.bridge/Editor/Controllers/EditorController.cs.meta +2 -0
  20. package/bridge/com.ucp.bridge/Editor/Controllers/EditorSettingsController.cs +527 -0
  21. package/bridge/com.ucp.bridge/Editor/Controllers/EditorSettingsController.cs.meta +2 -0
  22. package/bridge/com.ucp.bridge/Editor/Controllers/FileController.cs +141 -0
  23. package/bridge/com.ucp.bridge/Editor/Controllers/FileController.cs.meta +2 -0
  24. package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs +326 -0
  25. package/bridge/com.ucp.bridge/Editor/Controllers/HierarchyController.cs.meta +2 -0
  26. package/bridge/com.ucp.bridge/Editor/Controllers/ImporterController.cs +209 -0
  27. package/bridge/com.ucp.bridge/Editor/Controllers/ImporterController.cs.meta +2 -0
  28. package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +409 -0
  29. package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs.meta +2 -0
  30. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs +354 -0
  31. package/bridge/com.ucp.bridge/Editor/Controllers/MaterialController.cs.meta +2 -0
  32. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs +93 -0
  33. package/bridge/com.ucp.bridge/Editor/Controllers/ObjectReferenceResolver.cs.meta +2 -0
  34. package/bridge/com.ucp.bridge/Editor/Controllers/PackagesController.cs +503 -0
  35. package/bridge/com.ucp.bridge/Editor/Controllers/PackagesController.cs.meta +2 -0
  36. package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +188 -0
  37. package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs.meta +2 -0
  38. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs +260 -0
  39. package/bridge/com.ucp.bridge/Editor/Controllers/PrefabController.cs.meta +2 -0
  40. package/bridge/com.ucp.bridge/Editor/Controllers/ProfilerController.cs +1679 -0
  41. package/bridge/com.ucp.bridge/Editor/Controllers/ProfilerController.cs.meta +2 -0
  42. package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs +579 -0
  43. package/bridge/com.ucp.bridge/Editor/Controllers/PropertyController.cs.meta +2 -0
  44. package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs +166 -0
  45. package/bridge/com.ucp.bridge/Editor/Controllers/SceneChangeTracker.cs.meta +2 -0
  46. package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +318 -0
  47. package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs.meta +2 -0
  48. package/bridge/com.ucp.bridge/Editor/Controllers/ScreenshotController.cs +125 -0
  49. package/bridge/com.ucp.bridge/Editor/Controllers/ScreenshotController.cs.meta +2 -0
  50. package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs +104 -0
  51. package/bridge/com.ucp.bridge/Editor/Controllers/ScriptController.cs.meta +2 -0
  52. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +227 -0
  53. package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs.meta +2 -0
  54. package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +240 -0
  55. package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs.meta +2 -0
  56. package/bridge/com.ucp.bridge/Editor/Controllers/VcsController.cs +611 -0
  57. package/bridge/com.ucp.bridge/Editor/Controllers/VcsController.cs.meta +2 -0
  58. package/bridge/com.ucp.bridge/Editor/Controllers.meta +8 -0
  59. package/bridge/com.ucp.bridge/Editor/Protocol/CommandRouter.cs +53 -0
  60. package/bridge/com.ucp.bridge/Editor/Protocol/CommandRouter.cs.meta +2 -0
  61. package/bridge/com.ucp.bridge/Editor/Protocol/MessageTypes.cs +80 -0
  62. package/bridge/com.ucp.bridge/Editor/Protocol/MessageTypes.cs.meta +2 -0
  63. package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs +358 -0
  64. package/bridge/com.ucp.bridge/Editor/Protocol/MiniJson.cs.meta +2 -0
  65. package/bridge/com.ucp.bridge/Editor/Protocol.meta +8 -0
  66. package/bridge/com.ucp.bridge/Editor/Scripts/IUCPScript.cs +37 -0
  67. package/bridge/com.ucp.bridge/Editor/Scripts/IUCPScript.cs.meta +2 -0
  68. package/bridge/com.ucp.bridge/Editor/Scripts.meta +8 -0
  69. package/bridge/com.ucp.bridge/Editor/UCP.Bridge.Editor.asmdef +18 -0
  70. package/bridge/com.ucp.bridge/Editor/UCP.Bridge.Editor.asmdef.meta +7 -0
  71. package/bridge/com.ucp.bridge/Editor.meta +8 -0
  72. package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef +14 -0
  73. package/bridge/com.ucp.bridge/Runtime/UCP.Bridge.Runtime.asmdef.meta +7 -0
  74. package/bridge/com.ucp.bridge/Runtime.meta +8 -0
  75. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +1085 -0
  76. package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs.meta +2 -0
  77. package/bridge/com.ucp.bridge/Tests/Editor/UCP.Bridge.Editor.Tests.asmdef +12 -0
  78. package/bridge/com.ucp.bridge/Tests/Editor/UCP.Bridge.Editor.Tests.asmdef.meta +7 -0
  79. package/bridge/com.ucp.bridge/Tests/Editor.meta +8 -0
  80. package/bridge/com.ucp.bridge/Tests.meta +8 -0
  81. package/bridge/com.ucp.bridge/package.json +29 -0
  82. package/bridge/com.ucp.bridge/package.json.meta +7 -0
  83. package/package.json +2 -2
  84. package/scripts/install.js +4 -6
@@ -0,0 +1,583 @@
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.4.6";
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
+ // Editor lifecycle
112
+ EditorController.Register(s_router);
113
+
114
+ // Scenes
115
+ SceneController.Register(s_router);
116
+
117
+ // Snapshots
118
+ SnapshotController.Register(s_router);
119
+
120
+ // Screenshots
121
+ ScreenshotController.Register(s_router);
122
+
123
+ // Logs
124
+ LogsController.Register(s_router);
125
+
126
+ // Tests
127
+ TestRunnerController.Register(s_router);
128
+
129
+ // Profiler
130
+ ProfilerController.Register(s_router);
131
+
132
+ // Scripts (exec)
133
+ ScriptController.Register(s_router);
134
+
135
+ // Files
136
+ FileController.Register(s_router);
137
+
138
+ // Version Control (Unity VCS / Plastic SCM)
139
+ VcsController.Register(s_router);
140
+
141
+ // Object Properties
142
+ PropertyController.Register(s_router);
143
+
144
+ // Hierarchy Operations
145
+ HierarchyController.Register(s_router);
146
+
147
+ // Asset Management
148
+ AssetController.Register(s_router);
149
+ ImporterController.Register(s_router);
150
+
151
+ // Editor Settings (Player, Quality, Physics, Lighting, Tags/Layers)
152
+ EditorSettingsController.Register(s_router);
153
+
154
+ // Material Properties
155
+ MaterialController.Register(s_router);
156
+
157
+ // Prefab Operations
158
+ PrefabController.Register(s_router);
159
+
160
+ // Build Pipeline
161
+ BuildController.Register(s_router);
162
+
163
+ // Package management
164
+ PackagesController.Register(s_router);
165
+ }
166
+
167
+ private static void StartServer()
168
+ {
169
+ s_cts = new CancellationTokenSource();
170
+
171
+ for (int port = DefaultPort; port <= MaxPort; port++)
172
+ {
173
+ try
174
+ {
175
+ s_listener = new TcpListener(IPAddress.Loopback, port);
176
+ s_listener.Start();
177
+ s_port = port;
178
+ s_running = true;
179
+ Debug.Log($"[UCP] Bridge server started on port {port}");
180
+ break;
181
+ }
182
+ catch (Exception)
183
+ {
184
+ s_listener?.Stop();
185
+ s_listener = null;
186
+ }
187
+ }
188
+
189
+ if (!s_running)
190
+ {
191
+ Debug.LogError("[UCP] Failed to start bridge server - all ports in use");
192
+ return;
193
+ }
194
+
195
+ WriteLockFile();
196
+ Task.Run(() => AcceptLoop(s_cts.Token));
197
+ }
198
+
199
+ private static async Task AcceptLoop(CancellationToken ct)
200
+ {
201
+ while (!ct.IsCancellationRequested && s_running)
202
+ {
203
+ try
204
+ {
205
+ var tcp = await s_listener.AcceptTcpClientAsync();
206
+ var stream = tcp.GetStream();
207
+
208
+ // Read HTTP upgrade request (may arrive in multiple segments)
209
+ var requestBuilder = new StringBuilder();
210
+ var buffer = new byte[4096];
211
+ do
212
+ {
213
+ int read = await stream.ReadAsync(buffer, 0, buffer.Length, ct);
214
+ if (read == 0) { tcp.Close(); continue; }
215
+ requestBuilder.Append(Encoding.UTF8.GetString(buffer, 0, read));
216
+ }
217
+ while (!requestBuilder.ToString().Contains("\r\n\r\n") && stream.DataAvailable);
218
+
219
+ var request = requestBuilder.ToString();
220
+
221
+ // Check if it's a WebSocket upgrade
222
+ if (!request.Contains("Upgrade: websocket", StringComparison.OrdinalIgnoreCase))
223
+ {
224
+ var resp = "HTTP/1.1 426 Upgrade Required\r\nContent-Length: 0\r\n\r\n";
225
+ var respBytes = Encoding.UTF8.GetBytes(resp);
226
+ await stream.WriteAsync(respBytes, 0, respBytes.Length, ct);
227
+ tcp.Close();
228
+ continue;
229
+ }
230
+
231
+ // Extract Sec-WebSocket-Key
232
+ var keyMatch = Regex.Match(request, @"Sec-WebSocket-Key:\s*(\S+)",
233
+ RegexOptions.IgnoreCase);
234
+ if (!keyMatch.Success)
235
+ {
236
+ Debug.LogError("[UCP] No Sec-WebSocket-Key found in request");
237
+ tcp.Close();
238
+ continue;
239
+ }
240
+
241
+ var wsKey = keyMatch.Groups[1].Value.Trim();
242
+ var acceptKey = ComputeWebSocketAcceptKey(wsKey);
243
+
244
+ // Send upgrade response
245
+ var upgradeResponse =
246
+ "HTTP/1.1 101 Switching Protocols\r\n" +
247
+ "Upgrade: websocket\r\n" +
248
+ "Connection: Upgrade\r\n" +
249
+ $"Sec-WebSocket-Accept: {acceptKey}\r\n\r\n";
250
+ var upgradeBytes = Encoding.UTF8.GetBytes(upgradeResponse);
251
+ await stream.WriteAsync(upgradeBytes, 0, upgradeBytes.Length, ct);
252
+
253
+ // Create WebSocket from stream
254
+ var ws = WebSocket.CreateFromStream(stream, true, null, TimeSpan.FromSeconds(30));
255
+
256
+ lock (s_clientLock)
257
+ {
258
+ if (s_clients.Count >= MaxConnections)
259
+ {
260
+ _ = ws.CloseAsync(WebSocketCloseStatus.PolicyViolation,
261
+ "Max connections reached", CancellationToken.None);
262
+ continue;
263
+ }
264
+ s_clients.Add(ws);
265
+ }
266
+
267
+ _ = Task.Run(() => HandleClient(ws, ct));
268
+ }
269
+ catch (ObjectDisposedException) { break; }
270
+ catch (SocketException) { break; }
271
+ catch (Exception ex)
272
+ {
273
+ if (!ct.IsCancellationRequested)
274
+ Debug.LogError($"[UCP] Accept error: {ex.Message}");
275
+ }
276
+ }
277
+ }
278
+
279
+ private static string ComputeWebSocketAcceptKey(string key)
280
+ {
281
+ const string magic = "258EAFA5-E914-47DA-95CA-5AB5DC85B11B";
282
+ var combined = key + magic;
283
+ var hash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(combined));
284
+ return Convert.ToBase64String(hash);
285
+ }
286
+
287
+ private static async Task HandleClient(WebSocket ws, CancellationToken ct)
288
+ {
289
+ var buffer = new byte[64 * 1024]; // 64KB buffer
290
+
291
+ try
292
+ {
293
+ while (ws.State == WebSocketState.Open && !ct.IsCancellationRequested)
294
+ {
295
+ var segment = new ArraySegment<byte>(buffer);
296
+ WebSocketReceiveResult received;
297
+ var messageBuilder = new StringBuilder();
298
+
299
+ do
300
+ {
301
+ received = await ws.ReceiveAsync(segment, ct);
302
+ if (received.MessageType == WebSocketMessageType.Close)
303
+ {
304
+ await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
305
+ return;
306
+ }
307
+ messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, received.Count));
308
+ }
309
+ while (!received.EndOfMessage);
310
+
311
+ var message = messageBuilder.ToString();
312
+ ProcessMessage(ws, message);
313
+ }
314
+ }
315
+ catch (OperationCanceledException) { }
316
+ catch (WebSocketException) { }
317
+ catch (Exception ex)
318
+ {
319
+ Debug.LogError($"[UCP] Client error: {ex.Message}");
320
+ }
321
+ finally
322
+ {
323
+ lock (s_clientLock)
324
+ {
325
+ s_clients.Remove(ws);
326
+ s_logSubscribers.Remove(ws);
327
+ }
328
+ }
329
+ }
330
+
331
+ private static void ProcessMessage(WebSocket ws, string message)
332
+ {
333
+ // Parse JSON-RPC request
334
+ long id = 0;
335
+ string method = null;
336
+ string paramsJson = "{}";
337
+
338
+ try
339
+ {
340
+ // Minimal JSON parsing using Unity's JsonUtility won't work well for dynamic JSON.
341
+ // Use a simple manual parse for the top-level fields.
342
+ var json = MiniJson.Deserialize(message) as Dictionary<string, object>;
343
+ if (json == null)
344
+ {
345
+ SendError(ws, 0, ErrorCodes.ParseError, "Invalid JSON");
346
+ return;
347
+ }
348
+
349
+ if (json.TryGetValue("id", out var idVal))
350
+ id = Convert.ToInt64(idVal);
351
+
352
+ if (json.TryGetValue("method", out var methodVal))
353
+ method = methodVal as string;
354
+
355
+ if (json.TryGetValue("params", out var paramsVal))
356
+ paramsJson = MiniJson.Serialize(paramsVal);
357
+ }
358
+ catch (Exception ex)
359
+ {
360
+ SendError(ws, 0, ErrorCodes.ParseError, $"Parse error: {ex.Message}");
361
+ return;
362
+ }
363
+
364
+ if (string.IsNullOrEmpty(method))
365
+ {
366
+ SendError(ws, id, ErrorCodes.InvalidRequest, "Missing 'method' field");
367
+ return;
368
+ }
369
+
370
+ // Handle log subscription specially
371
+ if (method == "logs/subscribe")
372
+ {
373
+ lock (s_clientLock) { s_logSubscribers.Add(ws); }
374
+ }
375
+ else if (method == "logs/unsubscribe")
376
+ {
377
+ lock (s_clientLock) { s_logSubscribers.Remove(ws); }
378
+ }
379
+
380
+ // Dispatch on main thread
381
+ var capturedId = id;
382
+ var capturedMethod = method;
383
+ var capturedParams = paramsJson;
384
+ var capturedWs = ws;
385
+
386
+ s_mainThreadQueue.Enqueue(() =>
387
+ {
388
+ var response = s_router.Dispatch(capturedMethod, capturedId, capturedParams);
389
+ SendResponse(capturedWs, response);
390
+ });
391
+ }
392
+
393
+ private static void SendResponse(WebSocket ws, JsonRpcResponse response)
394
+ {
395
+ try
396
+ {
397
+ var json = MiniJson.Serialize(ResponseToDict(response));
398
+ var bytes = Encoding.UTF8.GetBytes(json);
399
+ _ = ws.SendAsync(new ArraySegment<byte>(bytes),
400
+ WebSocketMessageType.Text, true, CancellationToken.None);
401
+ }
402
+ catch (Exception ex)
403
+ {
404
+ Debug.LogError($"[UCP] Send error: {ex.Message}");
405
+ }
406
+ }
407
+
408
+ private static void SendError(WebSocket ws, long id, int code, string message)
409
+ {
410
+ SendResponse(ws, JsonRpcResponse.Error(id, code, message));
411
+ }
412
+
413
+ /// <summary>
414
+ /// Send a notification to log subscribers only.
415
+ /// </summary>
416
+ public static void SendNotification(string method, object data)
417
+ {
418
+ SendNotificationTo(method, data, false);
419
+ }
420
+
421
+ /// <summary>
422
+ /// Broadcast a notification to ALL connected clients.
423
+ /// Use for test results and other non-log notifications.
424
+ /// </summary>
425
+ public static void BroadcastNotification(string method, object data)
426
+ {
427
+ SendNotificationTo(method, data, true);
428
+ }
429
+
430
+ private static void SendNotificationTo(string method, object data, bool toAll)
431
+ {
432
+ var dict = new Dictionary<string, object>
433
+ {
434
+ ["jsonrpc"] = "2.0",
435
+ ["method"] = method,
436
+ ["params"] = data
437
+ };
438
+ var json = MiniJson.Serialize(dict);
439
+ var bytes = Encoding.UTF8.GetBytes(json);
440
+ var segment = new ArraySegment<byte>(bytes);
441
+
442
+ List<WebSocket> targets;
443
+ lock (s_clientLock)
444
+ {
445
+ targets = toAll
446
+ ? new List<WebSocket>(s_clients)
447
+ : new List<WebSocket>(s_logSubscribers);
448
+ }
449
+
450
+ foreach (var ws in targets)
451
+ {
452
+ try
453
+ {
454
+ if (ws.State == WebSocketState.Open)
455
+ {
456
+ _ = ws.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None);
457
+ }
458
+ }
459
+ catch { /* ignore send failures for notifications */ }
460
+ }
461
+ }
462
+
463
+ private static void OnLogMessage(string message, string stackTrace, LogType type)
464
+ {
465
+ // Don't forward our own log messages to avoid infinite recursion
466
+ if (message.StartsWith("[UCP]")) return;
467
+
468
+ SendNotification("log", LogsController.RecordLog(message, stackTrace, type));
469
+ }
470
+
471
+ private static void PumpMainThread()
472
+ {
473
+ int processed = 0;
474
+ while (s_mainThreadQueue.TryDequeue(out var action) && processed < 50)
475
+ {
476
+ try { action(); }
477
+ catch (Exception ex) { Debug.LogError($"[UCP] Main thread error: {ex}"); }
478
+ processed++;
479
+ }
480
+ }
481
+
482
+ private static void WriteLockFile()
483
+ {
484
+ try
485
+ {
486
+ var projectPath = Path.GetDirectoryName(Application.dataPath);
487
+ var ucpDir = Path.Combine(projectPath, ".ucp");
488
+ Directory.CreateDirectory(ucpDir);
489
+
490
+ var lockData = new Dictionary<string, object>
491
+ {
492
+ ["pid"] = System.Diagnostics.Process.GetCurrentProcess().Id,
493
+ ["port"] = s_port,
494
+ ["protocolVersion"] = ProtocolVersion,
495
+ ["unityVersion"] = Application.unityVersion,
496
+ ["projectPath"] = projectPath,
497
+ ["startedAt"] = DateTime.UtcNow.ToString("o"),
498
+ ["token"] = s_token
499
+ };
500
+
501
+ File.WriteAllText(
502
+ Path.Combine(ucpDir, "bridge.lock"),
503
+ MiniJson.Serialize(lockData)
504
+ );
505
+ }
506
+ catch (Exception ex)
507
+ {
508
+ Debug.LogError($"[UCP] Failed to write lock file: {ex.Message}");
509
+ }
510
+ }
511
+
512
+ private static void CleanLockFile()
513
+ {
514
+ try
515
+ {
516
+ var projectPath = Path.GetDirectoryName(Application.dataPath);
517
+ var lockPath = Path.Combine(projectPath, ".ucp", "bridge.lock");
518
+ if (File.Exists(lockPath))
519
+ File.Delete(lockPath);
520
+ }
521
+ catch { }
522
+ }
523
+
524
+ private static void Shutdown()
525
+ {
526
+ if (!s_running) return;
527
+ s_running = false;
528
+
529
+ Debug.Log("[UCP] Bridge server shutting down");
530
+
531
+ s_cts?.Cancel();
532
+
533
+ // Stop listener first to release port immediately
534
+ try { s_listener?.Stop(); }
535
+ catch { }
536
+ s_listener = null;
537
+
538
+ lock (s_clientLock)
539
+ {
540
+ foreach (var ws in s_clients)
541
+ {
542
+ try { ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Shutting down", CancellationToken.None); }
543
+ catch { }
544
+ }
545
+ s_clients.Clear();
546
+ s_logSubscribers.Clear();
547
+ }
548
+
549
+ s_cts?.Dispose();
550
+ s_cts = null;
551
+
552
+ CleanLockFile();
553
+
554
+ EditorApplication.update -= PumpMainThread;
555
+ Application.logMessageReceived -= OnLogMessage;
556
+ }
557
+
558
+ private static Dictionary<string, object> ResponseToDict(JsonRpcResponse r)
559
+ {
560
+ var dict = new Dictionary<string, object>
561
+ {
562
+ ["jsonrpc"] = "2.0",
563
+ ["id"] = r.id
564
+ };
565
+
566
+ if (r.error != null)
567
+ {
568
+ dict["error"] = new Dictionary<string, object>
569
+ {
570
+ ["code"] = r.error.code,
571
+ ["message"] = r.error.message,
572
+ ["data"] = r.error.data
573
+ };
574
+ }
575
+ else
576
+ {
577
+ dict["result"] = r.result;
578
+ }
579
+
580
+ return dict;
581
+ }
582
+ }
583
+ }
@@ -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:
@@ -0,0 +1,18 @@
1
+ using UnityEditor;
2
+ using UnityEngine;
3
+
4
+ namespace UCP.Bridge
5
+ {
6
+ internal static class UnityObjectCompat
7
+ {
8
+ public static Object ResolveByInstanceId(int instanceId)
9
+ {
10
+ return EditorUtility.InstanceIDToObject(instanceId);
11
+ }
12
+
13
+ public static T ResolveByInstanceId<T>(int instanceId) where T : Object
14
+ {
15
+ return ResolveByInstanceId(instanceId) as T;
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 26985214c906f3945a01cd11320484e4
@@ -0,0 +1,8 @@
1
+ fileFormatVersion: 2
2
+ guid: 6e64b956356efe640939d405c9429117
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant: