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 _);
}
}
}