@@ -24,13 +24,31 @@ public static partial class UnityMcpBridge
2424 private static readonly object lockObj = new ( ) ;
2525 private static readonly object startStopLock = new ( ) ;
2626 private static bool initScheduled = false ;
27+ private static bool ensureUpdateHooked = false ;
28+ private static bool isStarting = false ;
29+ private static double nextStartAt = 0.0f ;
2730 private static double nextHeartbeatAt = 0.0f ;
31+ private static int heartbeatSeq = 0 ;
2832 private static Dictionary <
2933 string ,
3034 ( string commandJson , TaskCompletionSource < string > tcs )
3135 > commandQueue = new ( ) ;
3236 private static int currentUnityPort = 6400 ; // Dynamic port, starts with default
3337 private static bool isAutoConnectMode = false ;
38+
39+ // Debug helpers
40+ private static bool IsDebugEnabled ( )
41+ {
42+ try { return EditorPrefs . GetBool ( "UnityMCP.DebugLogs" , false ) ; } catch { return false ; }
43+ }
44+
45+ private static void LogBreadcrumb ( string stage )
46+ {
47+ if ( IsDebugEnabled ( ) )
48+ {
49+ Debug . Log ( $ "<b><color=#2EA3FF>UNITY-MCP</color></b>: [{ stage } ]") ;
50+ }
51+ }
3452
3553 public static bool IsRunning => isRunning ;
3654 public static int GetCurrentPort ( ) => currentUnityPort ;
@@ -78,11 +96,24 @@ public static bool FolderExists(string path)
7896
7997 static UnityMcpBridge ( )
8098 {
81- // Immediate start for minimal downtime, plus quit hook
82- Start ( ) ;
99+ // Skip bridge in headless/batch environments (CI/builds)
100+ if ( Application . isBatchMode )
101+ {
102+ return ;
103+ }
104+ // Defer start until the editor is idle and not compiling
105+ ScheduleInitRetry ( ) ;
106+ // Add a safety net update hook in case delayCall is missed during reload churn
107+ if ( ! ensureUpdateHooked )
108+ {
109+ ensureUpdateHooked = true ;
110+ EditorApplication . update += EnsureStartedOnEditorIdle ;
111+ }
83112 EditorApplication . quitting += Stop ;
84113 AssemblyReloadEvents . beforeAssemblyReload += OnBeforeAssemblyReload ;
85114 AssemblyReloadEvents . afterAssemblyReload += OnAfterAssemblyReload ;
115+ // Also coalesce play mode transitions into a deferred init
116+ EditorApplication . playModeStateChanged += _ => ScheduleInitRetry ( ) ;
86117 }
87118
88119 /// <summary>
@@ -94,7 +125,7 @@ private static void InitializeAfterCompilation()
94125 initScheduled = false ;
95126
96127 // Play-mode friendly: allow starting in play mode; only defer while compiling
97- if ( EditorApplication . isCompiling )
128+ if ( IsCompiling ( ) )
98129 {
99130 ScheduleInitRetry ( ) ;
100131 return ;
@@ -118,9 +149,77 @@ private static void ScheduleInitRetry()
118149 return ;
119150 }
120151 initScheduled = true ;
152+ // Debounce: start ~200ms after the last trigger
153+ nextStartAt = EditorApplication . timeSinceStartup + 0.20f ;
154+ // Ensure the update pump is active
155+ if ( ! ensureUpdateHooked )
156+ {
157+ ensureUpdateHooked = true ;
158+ EditorApplication . update += EnsureStartedOnEditorIdle ;
159+ }
160+ // Keep the original delayCall as a secondary path
121161 EditorApplication . delayCall += InitializeAfterCompilation ;
122162 }
123163
164+ // Safety net: ensure the bridge starts shortly after domain reload when editor is idle
165+ private static void EnsureStartedOnEditorIdle ( )
166+ {
167+ // Do nothing while compiling
168+ if ( IsCompiling ( ) )
169+ {
170+ return ;
171+ }
172+
173+ // If already running, remove the hook
174+ if ( isRunning )
175+ {
176+ EditorApplication . update -= EnsureStartedOnEditorIdle ;
177+ ensureUpdateHooked = false ;
178+ return ;
179+ }
180+
181+ // Debounced start: wait until the scheduled time
182+ if ( nextStartAt > 0 && EditorApplication . timeSinceStartup < nextStartAt )
183+ {
184+ return ;
185+ }
186+
187+ if ( isStarting )
188+ {
189+ return ;
190+ }
191+
192+ isStarting = true ;
193+ // Attempt start; if it succeeds, remove the hook to avoid overhead
194+ Start ( ) ;
195+ isStarting = false ;
196+ if ( isRunning )
197+ {
198+ EditorApplication . update -= EnsureStartedOnEditorIdle ;
199+ ensureUpdateHooked = false ;
200+ }
201+ }
202+
203+ // Helper to check compilation status across Unity versions
204+ private static bool IsCompiling ( )
205+ {
206+ if ( EditorApplication . isCompiling )
207+ {
208+ return true ;
209+ }
210+ try
211+ {
212+ System . Type pipeline = System . Type . GetType ( "UnityEditor.Compilation.CompilationPipeline, UnityEditor" ) ;
213+ var prop = pipeline ? . GetProperty ( "isCompiling" , System . Reflection . BindingFlags . Public | System . Reflection . BindingFlags . Static ) ;
214+ if ( prop != null )
215+ {
216+ return ( bool ) prop . GetValue ( null ) ;
217+ }
218+ }
219+ catch { }
220+ return false ;
221+ }
222+
124223 public static void Start ( )
125224 {
126225 lock ( startStopLock )
@@ -140,6 +239,9 @@ public static void Start()
140239 // Always consult PortManager first so we prefer the persisted project port
141240 currentUnityPort = PortManager . GetPortWithFallback ( ) ;
142241
242+ // Breadcrumb: Start
243+ LogBreadcrumb ( "Start" ) ;
244+
143245 const int maxImmediateRetries = 3 ;
144246 const int retrySleepMs = 75 ;
145247 int attempt = 0 ;
@@ -153,6 +255,13 @@ public static void Start()
153255 SocketOptionName . ReuseAddress ,
154256 true
155257 ) ;
258+ #if UNITY_EDITOR_WIN
259+ try
260+ {
261+ listener . ExclusiveAddressUse = false ;
262+ }
263+ catch { }
264+ #endif
156265 // Minimize TIME_WAIT by sending RST on close
157266 try
158267 {
@@ -180,6 +289,13 @@ public static void Start()
180289 SocketOptionName . ReuseAddress ,
181290 true
182291 ) ;
292+ #if UNITY_EDITOR_WIN
293+ try
294+ {
295+ listener . ExclusiveAddressUse = false ;
296+ }
297+ catch { }
298+ #endif
183299 try
184300 {
185301 listener . Server . LingerState = new LingerOption ( true , 0 ) ;
@@ -198,7 +314,8 @@ public static void Start()
198314 Task . Run ( ListenerLoop ) ;
199315 EditorApplication . update += ProcessCommands ;
200316 // Write initial heartbeat immediately
201- WriteHeartbeat ( false ) ;
317+ heartbeatSeq ++ ;
318+ WriteHeartbeat ( false , "ready" ) ;
202319 nextHeartbeatAt = EditorApplication . timeSinceStartup + 0.5f ;
203320 }
204321 catch ( SocketException ex )
@@ -571,16 +688,22 @@ private static string GetParamsSummary(JObject @params)
571688 // Heartbeat/status helpers
572689 private static void OnBeforeAssemblyReload ( )
573690 {
574- WriteHeartbeat ( true ) ;
691+ // Stop cleanly before reload so sockets close and clients see 'reloading'
692+ try { Stop ( ) ; } catch { }
693+ WriteHeartbeat ( true , "reloading" ) ;
694+ LogBreadcrumb ( "Reload" ) ;
575695 }
576696
577697 private static void OnAfterAssemblyReload ( )
578698 {
579699 // Will be overwritten by Start(), but mark as alive quickly
580- WriteHeartbeat ( false ) ;
700+ WriteHeartbeat ( false , "idle" ) ;
701+ LogBreadcrumb ( "Idle" ) ;
702+ // Schedule a safe restart after reload to avoid races during compilation
703+ ScheduleInitRetry ( ) ;
581704 }
582705
583- private static void WriteHeartbeat ( bool reloading )
706+ private static void WriteHeartbeat ( bool reloading , string reason = null )
584707 {
585708 try
586709 {
@@ -591,6 +714,8 @@ private static void WriteHeartbeat(bool reloading)
591714 {
592715 unity_port = currentUnityPort ,
593716 reloading ,
717+ reason = reason ?? ( reloading ? "reloading" : "ready" ) ,
718+ seq = heartbeatSeq ,
594719 project_path = Application . dataPath ,
595720 last_heartbeat = DateTime . UtcNow . ToString ( "O" )
596721 } ;
0 commit comments