/*
┌──────────────────────────────────────────────────────────────────┐
│ Author: Ivan Murzak (https://github.com/IvanMurzak) │
│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
│ Copyright (c) 2025 Ivan Murzak │
│ Licensed under the Apache License, Version 2.0. │
│ See the LICENSE file in the project root for more information. │
└──────────────────────────────────────────────────────────────────┘
*/
#nullable enable
using com.IvanMurzak.Unity.MCP.Runtime.Utils;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
using UnityEditor;
using UnityEngine;
namespace com.IvanMurzak.Unity.MCP.Editor
{
public static partial class Startup
{
static void SubscribeOnEditorEvents()
{
Application.unloading += OnApplicationUnloading;
Application.quitting += OnApplicationQuitting;
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload;
// Handle Play mode state changes to ensure reconnection after exiting Play mode
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
static void OnApplicationUnloading() => TryDisconnectAndCleanup(nameof(OnApplicationUnloading));
static void OnApplicationQuitting() => TryDisconnectAndCleanup(nameof(OnApplicationQuitting));
static void OnBeforeAssemblyReload() => TryDisconnectAndCleanup(nameof(OnBeforeAssemblyReload), onlyIfConnected: true);
///
/// Safely disconnects and cleans up the MCP plugin instance.
/// Catches exceptions to prevent blocking Unity's shutdown/reload process.
///
/// Name of the calling method for logging.
/// If true, only disconnects when in Connected state.
/// This prevents issues when cancelling in-progress connection attempts during assembly reload.
/// Note: Log collector disposal always occurs regardless of this flag.
static void TryDisconnectAndCleanup(string callerName, bool onlyIfConnected = false)
{
if (!UnityMcpPluginEditor.HasInstance)
{
_logger.LogDebug("{class} {method} triggered: No UnityMcpPluginEditor instance to disconnect",
nameof(Startup), callerName);
return;
}
_logger.LogInformation("{method} triggered", callerName);
var plugin = UnityMcpPluginEditor.Instance;
if (plugin.HasMcpPluginInstance)
{
var connectionState = UnityMcpPluginEditor.ConnectionState.CurrentValue;
// When onlyIfConnected is true, skip disconnect unless we have an established connection.
// This prevents hanging when cancelling in-progress connection attempts (Connecting/Reconnecting states).
if (onlyIfConnected && connectionState != HubConnectionState.Connected)
{
_logger.LogTrace("Skipping {method} - not connected (state: {state})",
nameof(plugin.DisconnectImmediate), connectionState);
}
else
{
try
{
plugin.DisconnectImmediate();
}
catch (System.Exception e)
{
_logger.LogWarning(e, "{class} {method}: Exception during disconnect (non-blocking): {message}",
nameof(Startup), callerName, e.Message);
}
}
}
try
{
plugin.DisposeLogCollector();
}
catch (System.Exception e)
{
_logger.LogWarning(e, "{class} {method}: Exception during log collector disposal (non-blocking): {message}",
nameof(Startup), callerName, e.Message);
}
if (UnityMcpPluginRuntime.HasInstance)
{
try
{
UnityMcpPluginRuntime.DisposeInstance();
}
catch (System.Exception e)
{
_logger.LogWarning(e, "{class} {method}: Exception during runtime MCP plugin disposal (non-blocking): {message}",
nameof(Startup), callerName, e.Message);
}
}
}
static void OnAfterAssemblyReload()
{
var isCi = EnvironmentUtils.IsCi();
var keepConnected = UnityMcpPluginEditor.KeepConnected;
var connectionAllowed = !isCi || keepConnected;
_logger.LogInformation("{method} triggered - BuildAndStart with connectionAllowed: {connectionAllowed} (isCi: {isCi}, keepConnected: {keepConnected})",
nameof(OnAfterAssemblyReload), connectionAllowed, isCi, keepConnected);
UnityMcpPluginEditor.Instance.BuildMcpPluginIfNeeded();
UnityMcpPluginEditor.Instance.AddUnityLogCollectorIfNeeded(() => new BufferedFileLogStorage());
if (connectionAllowed)
UnityMcpPluginEditor.ConnectIfNeeded();
}
static void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (!UnityMcpPluginEditor.HasInstance)
{
_logger.LogDebug("{class} {method} triggered: No UnityMcpPluginEditor instance available. State: {state}",
nameof(Startup), nameof(OnPlayModeStateChanged), state);
return;
}
// Log Play mode state changes for debugging
_logger.LogInformation("Play mode state changed: {state}", state);
switch (state)
{
case PlayModeStateChange.ExitingPlayMode:
// Dispose the runtime plugin instance created by Initialize().Build().
// This handles the no-domain-reload case; TryDisconnectAndCleanup covers the domain-reload case.
_logger.LogTrace("Exiting Play mode - disposing runtime MCP plugin instance if present");
if (UnityMcpPluginRuntime.HasInstance)
UnityMcpPluginRuntime.DisposeInstance();
break;
case PlayModeStateChange.EnteredEditMode:
// Unity has returned to Edit mode - ensure connection is re-established
// if the configuration expects it to be connected
var isCi = EnvironmentUtils.IsCi();
var keepConnected = UnityMcpPluginEditor.KeepConnected;
_logger.LogTrace("Entered Edit mode - KeepConnected: {keepConnected}, IsCi: {isCi}",
keepConnected, isCi);
if (isCi && !keepConnected)
{
_logger.LogTrace("Skipping reconnection in CI environment (KeepConnected is false)");
break;
}
_logger.LogTrace("Scheduling reconnection after Play mode exit");
// Small delay to ensure Unity is fully settled in Edit mode
EditorApplication.delayCall += () =>
{
_logger.LogTrace("Initiating delayed reconnection after Play mode exit");
UnityMcpPluginEditor.Instance.BuildMcpPluginIfNeeded();
UnityMcpPluginEditor.Instance.AddUnityLogCollectorIfNeeded(() => new BufferedFileLogStorage());
UnityMcpPluginEditor.ConnectIfNeeded();
};
// No delay, immediate reconnection for the case if Unity Editor in background
// (has no focus)
_logger.LogTrace("Initiating reconnection after Play mode exit");
UnityMcpPluginEditor.Instance.BuildMcpPluginIfNeeded();
UnityMcpPluginEditor.Instance.AddUnityLogCollectorIfNeeded(() => new BufferedFileLogStorage());
UnityMcpPluginEditor.ConnectIfNeeded();
break;
case PlayModeStateChange.ExitingEditMode:
_logger.LogTrace("Exiting Edit mode to enter Play mode");
break;
case PlayModeStateChange.EnteredPlayMode:
_logger.LogTrace("Entered Play mode");
break;
}
}
}
}