@@ -427,10 +427,9 @@ public override void BeginGui(Paper paper)
427427
428428 // Load user script assemblies and re-register all types
429429 ScriptAssemblyManager . LoadAssemblies ( Project . Current ) ;
430- ReinitializeRegistries ( ) ;
431430
432- // Load project settings
433- ProjectSettingsRegistry . OnProjectOpened ( ) ;
431+ // ReinitializeRegistries() runs the [OnAssemblyLoad] hooks, which include the project- settings reload.
432+ ReinitializeRegistries ( ) ;
434433
435434 // Restore layout from project (or use default)
436435 var savedLayout = LoadDockLayout ( ) ;
@@ -1116,6 +1115,30 @@ private void ScanAndRegisterPanels()
11161115 return FindInNode ( node . ChildA , panelType ) ?? FindInNode ( node . ChildB , panelType ) ;
11171116 }
11181117
1118+ /// <summary>Enumerate every open panel across the docked tree and all floating windows.</summary>
1119+ private IEnumerable < DockPanel > EnumerateAllPanels ( )
1120+ {
1121+ foreach ( var p in EnumerateNodePanels ( _dockSpace . Root ) )
1122+ yield return p ;
1123+ foreach ( var fw in _dockSpace . FloatingWindows )
1124+ foreach ( var p in EnumerateNodePanels ( fw . Node ) )
1125+ yield return p ;
1126+ }
1127+
1128+ private static IEnumerable < DockPanel > EnumerateNodePanels ( DockNode ? node )
1129+ {
1130+ if ( node == null ) yield break ;
1131+ if ( node . IsLeaf )
1132+ {
1133+ if ( node . Tabs != null )
1134+ foreach ( var tab in node . Tabs )
1135+ yield return tab ;
1136+ yield break ;
1137+ }
1138+ foreach ( var p in EnumerateNodePanels ( node . ChildA ) ) yield return p ;
1139+ foreach ( var p in EnumerateNodePanels ( node . ChildB ) ) yield return p ;
1140+ }
1141+
11191142 /// <summary>
11201143 /// Open a panel. If it's already open, focus it. Otherwise create a new instance as a floating window.
11211144 /// </summary>
@@ -1267,42 +1290,199 @@ private void RegisterMenus()
12671290 // Script Compilation
12681291 // ================================================================
12691292
1270- /// <summary>Called by ScriptAssemblyManager after hot-reload to re-scan all registries.</summary>
1271- public void ReinitializeAfterReload ( ) => ReinitializeRegistries ( ) ;
1293+ /// <summary>
1294+ /// Called by <see cref="ScriptAssemblyManager"/> right after the new script assemblies are
1295+ /// loaded. Re-scans every registry against the fresh assemblies and reloads project settings.
1296+ /// </summary>
1297+ public void ReinitializeAfterReload ( )
1298+ {
1299+ ReinitializeRegistries ( ) ;
1300+ }
12721301
1273- private void ReinitializeRegistries ( )
1302+ /// <summary>
1303+ /// Drops every strong reference the editor holds into the script <see cref="System.Runtime.Loader.AssemblyLoadContext"/>
1304+ /// so it can actually be collected when unloaded. This is the counterpart to
1305+ /// <see cref="ReinitializeAfterReload"/>: tear everything down here, rebuild it there.
1306+ ///
1307+ /// Anything that survives this call and transitively reaches a user type (a live instance, a
1308+ /// <see cref="Type"/> handle, a delegate bound to user code, a <see cref="FieldInfo"/>) pins
1309+ /// the old context and forces a full editor restart instead of a hot-reload.
1310+ /// </summary>
1311+ public void ReleaseScriptReferences ( )
12741312 {
1313+ CaptureSelectionForReload ( ) ;
1314+
1315+ // 1. Live object graph: the scene's GameObjects hold user MonoBehaviour instances.
1316+ // The scene was already serialized to disk by SaveSceneForRestart().
1317+ Selection . Clear ( ) ;
1318+ Undo . Clear ( ) ;
1319+ Runtime . Resources . Scene . Unload ( ) ;
1320+
1321+ // 1b. Long-lived editor panels cache scene objects (e.g. the Inspector's last target,
1322+ // the Hierarchy's drag target). Let each drop its references before the unload.
1323+ if ( _dockSpace != null )
1324+ foreach ( var panel in EnumerateAllPanels ( ) )
1325+ if ( panel is IScriptReloadCleanup cleanup )
1326+ try { cleanup . OnScriptReloadCleanup ( ) ; } catch { }
1327+
1328+ // Release Paper callbacks as they might otherwise pin ALC types across a reload.
1329+ ReleasePaperRetainedCallbacks ( ) ;
1330+
1331+ // 2. Play-mode leftovers (normally empty outside play mode; cleared defensively).
1332+ _savedEditorScene = null ;
1333+ _savedEditorTime = null ;
1334+ MenuRegistry . Clear ( ) ;
12751335 _registeredPanels . Clear ( ) ;
1276- ScanAndRegisterPanels ( ) ;
1277- InitializeOnLoadRegistry . Reinitialize ( ) ;
1278- PropertyEditorRegistry . Reinitialize ( ) ;
1279- CustomEditorRegistry . Reinitialize ( ) ;
1280- GraphTools . NodeRendererRegistry . Reinitialize ( ) ;
1281- GraphTools . NodePreviewRegistry . Reinitialize ( ) ;
1282- Runtime . GraphTools . GraphValidatorRegistry . Reinitialize ( ) ;
1283- Inspector . AssetImporterEditorRegistry . Reinitialize ( ) ;
1284- GUI . Popups . AddComponentPopup . Reinitialize ( ) ;
1285- Importers . ImporterRegistry . Reinitialize ( ) ;
1286- ProjectSettingsRegistry . Reinitialize ( ) ;
1287- CreateAssetMenuRegistry . Reinitialize ( ) ;
1288- ShaderTypeCreateMenu . Register ( ) ;
1289- ThumbnailGeneratorRegistry . Reinitialize ( ) ;
1290- SceneDropHandlerRegistry . Reinitialize ( ) ;
1291- CreateGameObjectMenuRegistry . Reinitialize ( ) ;
1292- FileIconRegistry . Reinitialize ( ) ;
1293- AssetDoubleClickRegistry . Reinitialize ( ) ;
1294- ScriptTemplateRegistry . Reinitialize ( ) ;
1295-
1296- // Re-register Window menu items for any new panels from user assemblies
1297- foreach ( var ( type , path ) in _registeredPanels )
1336+
1337+ // 3. The Echo serializer cache lives in an external package so we can't call OnAssemblyUnload there.
1338+ Echo . Serializer . ClearCache ( ) ;
1339+
1340+ // 4. Everything tagged [OnAssemblyUnload]
1341+ ScriptReloadCallbacks . InvokeAssemblyUnload ( ) ;
1342+ }
1343+
1344+ private void ReleasePaperRetainedCallbacks ( )
1345+ {
1346+ try
12981347 {
1299- var capturedType = type ;
1300- MenuRegistry . Register ( $ "Window/{ path } ", ( ) => OpenPanel ( capturedType ) ,
1301- isChecked : ( ) => IsPanelOpen ( capturedType ) ) ;
1348+ var paper = PaperInstance ;
1349+ if ( paper == null ) return ;
1350+
1351+ Type t = paper . GetType ( ) ;
1352+ const BindingFlags BF = BindingFlags . NonPublic | BindingFlags . Instance ;
1353+
1354+ if ( t . GetField ( "_elements" , BF ) ? . GetValue ( paper ) is not Array elements ) return ;
1355+
1356+ int count = t . GetField ( "_elementCount" , BF ) ? . GetValue ( paper ) is int c ? c : 0 ;
1357+ count = Math . Clamp ( count , 0 , elements . Length ) ;
1358+ if ( count < elements . Length )
1359+ Array . Clear ( elements , count , elements . Length - count ) ;
1360+ }
1361+ catch ( Exception ex )
1362+ {
1363+ Runtime . Debug . LogWarning ( $ "[EditorApplication] Could not reset PaperUI retained callbacks: { ex . Message } ") ;
13021364 }
1365+ }
13031366
1304- // Re-register GameObject menu items for any new creators from user assemblies
1305- CreateGameObjectMenuRegistry . RegisterMenuBarItems ( ) ;
1367+ // ================================================================
1368+ // Selection preserve/restore across a hot-reload
1369+ // ================================================================
1370+
1371+ private List < SelectionToken > ? _reloadSelection ;
1372+ private SelectionToken _reloadActive ;
1373+ private bool _hasReloadActive ;
1374+
1375+ /// <summary>
1376+ /// Snapshot the current selection as identifier tokens (called before the selection is cleared).
1377+ /// </summary>
1378+ private void CaptureSelectionForReload ( )
1379+ {
1380+ _reloadSelection = new List < SelectionToken > ( ) ;
1381+ _hasReloadActive = false ;
1382+
1383+ foreach ( var obj in Selection . Selected )
1384+ {
1385+ if ( ! TryMakeSelectionToken ( obj , out var token ) )
1386+ continue ;
1387+
1388+ _reloadSelection . Add ( token ) ;
1389+ if ( ReferenceEquals ( obj , Selection . ActiveObject ) )
1390+ {
1391+ _reloadActive = token ;
1392+ _hasReloadActive = true ;
1393+ }
1394+ }
1395+ }
1396+
1397+ /// <summary>
1398+ /// Tries to create a selection token to then restore the selection after script reload.
1399+ /// </summary>
1400+ private static bool TryMakeSelectionToken ( object obj , out SelectionToken token )
1401+ {
1402+ switch ( obj )
1403+ {
1404+ // Scene GameObject - restore by stable scene identifier.
1405+ case GameObject go :
1406+ token = new SelectionToken ( SelKind . GameObject , go . Identifier , Guid . Empty , "" , "" , false ) ;
1407+ return true ;
1408+ // Scene component - restore by owning GameObject + component identifier.
1409+ case MonoBehaviour mb when mb . GameObject . IsValid ( ) :
1410+ token = new SelectionToken ( SelKind . Component , mb . GameObject . Identifier , mb . Identifier , "" , "" , false ) ;
1411+ return true ;
1412+ // Project asset - restore by AssetID via the asset database.
1413+ case EngineObject eo when eo . AssetID != Guid . Empty :
1414+ token = new SelectionToken ( SelKind . Asset , eo . AssetID , Guid . Empty , "" , "" , false ) ;
1415+ return true ;
1416+ // Project browser item - identifier-only, rebuilt from its path/guid.
1417+ case ContentItem ci :
1418+ token = new SelectionToken ( SelKind . Content , ci . Guid , Guid . Empty , ci . RelativePath , ci . Name , ci . IsFolder ) ;
1419+ return true ;
1420+ default :
1421+ token = default ;
1422+ return false ;
1423+ }
1424+ }
1425+
1426+ /// <summary>
1427+ /// Re-resolve the captured selection tokens against the freshly reloaded scene/assets and re-select them.
1428+ /// </summary>
1429+ public void RestoreSelectionAfterReload ( )
1430+ {
1431+ if ( _reloadSelection == null )
1432+ return ;
1433+
1434+ var tokens = _reloadSelection ;
1435+ _reloadSelection = null ;
1436+
1437+ Selection . Clear ( ) ;
1438+ object ? active = null ;
1439+
1440+ foreach ( var token in tokens )
1441+ {
1442+ object ? resolved = ResolveSelectionToken ( token ) ;
1443+ if ( resolved == null )
1444+ continue ;
1445+
1446+ Selection . AddToSelection ( resolved ) ;
1447+ if ( _hasReloadActive && token . Equals ( _reloadActive ) )
1448+ active = resolved ;
1449+ }
1450+
1451+ if ( active != null )
1452+ Selection . ActiveObject = active ;
1453+
1454+ _hasReloadActive = false ;
1455+ }
1456+
1457+ private static object ? ResolveSelectionToken ( SelectionToken token )
1458+ {
1459+ switch ( token . Kind )
1460+ {
1461+ case SelKind . GameObject :
1462+ return Runtime . Resources . Scene . Current ? . FindObjectByIdentifier < GameObject > ( token . Id ) ;
1463+ case SelKind . Component :
1464+ return Runtime . Resources . Scene . Current ? . FindObjectByIdentifier < GameObject > ( token . Id ) ? . GetComponentByIdentifier ( token . CompId ) ;
1465+ case SelKind . Asset :
1466+ return Runtime . AssetDatabase . Get ( token . Id ) ;
1467+ case SelKind . Content :
1468+ // ContentItem compares by Guid + RelativePath, so a rebuilt instance re-selects the same item.
1469+ return new ContentItem { Guid = token . Id , RelativePath = token . Path , Name = token . Name , IsFolder = token . IsFolder } ;
1470+ default :
1471+ return null ;
1472+ }
1473+ }
1474+
1475+ private void ReinitializeRegistries ( )
1476+ {
1477+ // Panel scan is an editor-instance step (needed before the menu rebuild reads the panel list).
1478+ _registeredPanels . Clear ( ) ;
1479+ ScanAndRegisterPanels ( ) ;
1480+
1481+ // Run every [OnAssemblyLoad] hook
1482+ ScriptReloadCallbacks . InvokeAssemblyLoad ( ) ;
1483+
1484+ MenuRegistry . Clear ( ) ;
1485+ RegisterMenus ( ) ;
13061486 }
13071487
13081488 public void RestoreAutoSavedScene ( string path )
0 commit comments