using System; using Uigenx.Binding; using Uigenx.Bridge; using Uigenx.Protocol; using UnityEditor; using UnityEngine; namespace Uigenx.Editor { /// /// Runs the studio bridge inside the editor. The client is rebuilt after every domain reload; /// carries the "should be connected" flag across reloads so a /// script recompile does not silently disconnect the studio. /// [InitializeOnLoad] public static class UgxEditorBridge { const string ConnectedKey = "Uigenx.Editor.Bridge.Connected"; static UgxBridgeClient _client; static UgxDataStreamHandler _dataStream; static UgxActionHandler _actions; static UgxScreenStateHandler _screenState; static UgxHotReloadHandler _hotReload; static double _lastPumpTime; static UgxEditorBridge() { EditorApplication.update += Update; AssemblyReloadEvents.beforeAssemblyReload += Disconnect; EditorApplication.quitting += Disconnect; // A reload happened while connected, so reconnect once the editor settles. if (SessionState.GetBool(ConnectedKey, false)) EditorApplication.delayCall += Connect; } public static bool IsConnected => _client != null && _client.State == UgxBridgeState.Connected; public static UgxBridgeState State => _client?.State ?? UgxBridgeState.Disconnected; public static string DaemonUrl { get; private set; } /// True when a daemon.json with a token was found. The token itself is never exposed. public static bool HasToken { get; private set; } public static void Connect() { if (_client != null) return; var settings = UgxProjectSettings.Load(); if (!settings.BridgeEnabled) { Debug.Log("[ugx] the bridge is disabled in Project Settings > UI Creator"); return; } var projectPath = settings.ResolveUicProjectPath(); if (projectPath == null) { Debug.LogWarning("[ugx] no UI Creator project at '" + settings.UgxProject + "'. Set the path in Project Settings > UI Creator."); return; } var daemon = UgxDaemonInfo.Load(projectPath); if (daemon == null) { Debug.LogWarning("[ugx] no daemon is running: " + projectPath + "/" + UgxDaemonInfo.RelativePath + " is missing. Start the studio first."); return; } DaemonUrl = daemon.ResolveUrl(); HasToken = !string.IsNullOrEmpty(daemon.Token); var options = new UgxBridgeOptions { Url = DaemonUrl, Token = daemon.Token ?? string.Empty, Role = UgxRole.Editor, ProjectId = daemon.ProjectId ?? Application.productName, IsPlaying = EditorApplication.isPlaying, RenderPipeline = UgxBridgeBehaviour.DetectPipeline(), }; options.Capabilities.Add(UgxCapability.Sync); options.Capabilities.Add(UgxCapability.Compile); options.Capabilities.Add(UgxCapability.DataSchema); options.Capabilities.Add(UgxCapability.HotReload); options.Capabilities.Add(UgxCapability.Actions); options.Capabilities.Add(UgxCapability.LiveData); _client = new UgxBridgeClient(options); _client.Register(UgxMessageTypes.SyncBundle, OnSyncBundle); _client.Register(UgxMessageTypes.SyncFile, OnSyncFile); _client.Register(UgxMessageTypes.CompileRequest, OnCompileRequest); _client.Register(UgxMessageTypes.DataSchemaRequest, OnDataSchemaRequest); _dataStream = new UgxDataStreamHandler(_client, ResolveDataSource); _actions = new UgxActionHandler(_client); _screenState = new UgxScreenStateHandler(_client); _hotReload = new UgxHotReloadHandler(_client); UgxCompileReporter.Reported += OnCompileReported; _client.Start(); SessionState.SetBool(ConnectedKey, true); // A compilation that finished during the last domain reload parked its result. if (UgxCompileReporter.TryTakePending(out var pending)) _client.Send(UgxMessageTypes.CompileResult, pending); } public static void Disconnect() { if (_client == null) { SessionState.SetBool(ConnectedKey, false); return; } UgxCompileReporter.Reported -= OnCompileReported; _hotReload?.Dispose(); _screenState?.Dispose(); _actions?.Dispose(); _dataStream?.Dispose(); _client.Dispose(); _hotReload = null; _screenState = null; _actions = null; _dataStream = null; _client = null; } /// Disconnects and forgets the reconnect flag, so a reload stays disconnected. public static void Stop() { Disconnect(); SessionState.SetBool(ConnectedKey, false); } static void Update() { if (_client == null) return; _client.Pump(); _screenState.Tick(); double now = EditorApplication.timeSinceStartup; // A cleared field in the settings page writes 0, and 1/0 would stall the stream forever. float hz = Mathf.Clamp(UgxProjectSettings.Load().StreamHz, 1f, 240f); if (now - _lastPumpTime < 1d / hz) return; _lastPumpTime = now; _dataStream.Tick(now); } static IUgxDataSource ResolveDataSource() => UgxScreenManager.Instance?.DataSource; static void OnSyncBundle(UgxEnvelope envelope) { var bundle = envelope.BodyAs(); var settings = UgxProjectSettings.Load(); UgxSyncResult result; try { result = UgxBundleImporter.Import(bundle, settings.AssetsUiFolder); } catch (Exception e) { _client.SendError(UgxErrorCodes.ImportFailed, e.Message, envelope.Id); return; } UgxScreenRegistryBuilder.Rebuild(bundle.Manifest, settings.AssetsUiFolder); _client.Send(UgxMessageTypes.SyncResult, result, re: envelope.Id); } static void OnSyncFile(UgxEnvelope envelope) => UgxBundleImporter.AddChunk(envelope.BodyAs()); static void OnCompileRequest(UgxEnvelope envelope) { var body = envelope.BodyAs(); UgxCompileReporter.ExpectRequest(body.RequestId); if (EditorApplication.isCompiling) { // The result arrives from the reporter when the current compilation finishes. return; } _client.Send(UgxMessageTypes.CompileResult, UgxCompileReporter.Snapshot(body.RequestId), re: envelope.Id); } static void OnDataSchemaRequest(UgxEnvelope envelope) { var body = envelope.BodyAs(); var result = UgxSchemaReflector.Reflect(body.Type, body.RequestId); _client.Send(UgxMessageTypes.DataSchemaResult, result, re: envelope.Id); } static void OnCompileReported(UgxCompileResult result) { if (_client == null) return; _client.Send(UgxMessageTypes.CompileResult, result); // Delivered live, so the parked copy must not be sent again on the next connect. UgxCompileReporter.TryTakePending(out _); } } }