diff --git a/Source/Editor/CustomEditors/CustomEditorPresenter.cs b/Source/Editor/CustomEditors/CustomEditorPresenter.cs index a5d041a97..d3533528a 100644 --- a/Source/Editor/CustomEditors/CustomEditorPresenter.cs +++ b/Source/Editor/CustomEditors/CustomEditorPresenter.cs @@ -196,6 +196,15 @@ namespace FlaxEditor.CustomEditors Presenter.AfterLayout?.Invoke(layout); } + /// + protected override void Deinitialize() + { + Editor = null; + _overrideEditor = null; + + base.Deinitialize(); + } + /// protected override void OnModified() { diff --git a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs index a93969e36..e7c8d2699 100644 --- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs @@ -205,7 +205,7 @@ namespace FlaxEditor.CustomEditors.Dedicated if (_linkedPrefabId != Guid.Empty) { _linkedPrefabId = Guid.Empty; - Editor.Instance.Prefabs.PrefabApplied -= OnPrefabApplying; + Editor.Instance.Prefabs.PrefabApplying -= OnPrefabApplying; Editor.Instance.Prefabs.PrefabApplied -= OnPrefabApplied; } } diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs index 583241915..b0239dc8f 100644 --- a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs @@ -1057,6 +1057,7 @@ namespace FlaxEditor.CustomEditors.Dedicated protected override void Deinitialize() { _scriptToggles = null; + _scripts.Clear(); base.Deinitialize(); } diff --git a/Source/Editor/CustomEditors/Editors/GenericEditor.cs b/Source/Editor/CustomEditors/Editors/GenericEditor.cs index bfcd8d046..aba1ddb81 100644 --- a/Source/Editor/CustomEditors/Editors/GenericEditor.cs +++ b/Source/Editor/CustomEditors/Editors/GenericEditor.cs @@ -819,6 +819,15 @@ namespace FlaxEditor.CustomEditors.Editors OnGroupsEnd(); } + /// + protected override void Deinitialize() + { + _visibleIfCaches = null; + _visibleIfPropertiesListsCache = null; + + base.Deinitialize(); + } + /// public override void Refresh() { diff --git a/Source/Editor/GUI/Docking/DockWindow.cs b/Source/Editor/GUI/Docking/DockWindow.cs index 937d6232f..88ae18df7 100644 --- a/Source/Editor/GUI/Docking/DockWindow.cs +++ b/Source/Editor/GUI/Docking/DockWindow.cs @@ -464,7 +464,7 @@ namespace FlaxEditor.GUI.Docking { base.Focus(); - SelectTab(); + SelectTab(false); BringToFront(); } diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs index 4de7b593b..a21fb3b53 100644 --- a/Source/Editor/Modules/ContentDatabaseModule.cs +++ b/Source/Editor/Modules/ContentDatabaseModule.cs @@ -21,6 +21,7 @@ namespace FlaxEditor.Modules private bool _enableEvents; private bool _isDuringFastSetup; private bool _rebuildFlag; + private bool _rebuildInitFlag; private int _itemsCreated; private int _itemsDeleted; private readonly HashSet _dirtyNodes = new HashSet(); @@ -61,7 +62,7 @@ namespace FlaxEditor.Modules public event Action WorkspaceModified; /// - /// Occurs when workspace has will be rebuilt. + /// Occurs when workspace will be rebuilt. /// public event Action WorkspaceRebuilding; @@ -88,6 +89,9 @@ namespace FlaxEditor.Modules // Register AssetItems serialization helper (serialize ref ID only) FlaxEngine.Json.JsonSerializer.Settings.Converters.Add(new AssetItemConverter()); + + ScriptsBuilder.ScriptsReload += OnScriptsReload; + ScriptsBuilder.ScriptsReloadEnd += OnScriptsReloadEnd; } private void OnContentAssetDisposing(Asset asset) @@ -817,6 +821,7 @@ namespace FlaxEditor.Modules Profiler.BeginEvent("ContentDatabase.Rebuild"); var startTime = Platform.TimeSeconds; _rebuildFlag = false; + _rebuildInitFlag = false; _enableEvents = false; // Load all folders @@ -1230,8 +1235,6 @@ namespace FlaxEditor.Modules LoadProjects(Game.Project); } - RebuildInternal(); - Editor.ContentImporting.ImportFileEnd += (obj, failed) => { var path = obj.ResultUrl; @@ -1239,6 +1242,15 @@ namespace FlaxEditor.Modules FlaxEngine.Scripting.InvokeOnUpdate(() => OnImportFileDone(path)); }; _enableEvents = true; + _rebuildInitFlag = true; + } + + /// + public override void OnEndInit() + { + // Handle init when project was loaded without scripts loading () + if (_rebuildInitFlag) + RebuildInternal(); } private void OnImportFileDone(string path) @@ -1313,6 +1325,52 @@ namespace FlaxEditor.Modules } } + private void OnScriptsReload() + { + var enabledEvents = _enableEvents; + _enableEvents = false; + _isDuringFastSetup = true; + var startItems = _itemsCreated; + foreach (var project in Projects) + { + if (project.Content != null) + { + //Dispose(project.Content.Folder); + for (int i = 0; i < project.Content.Folder.Children.Count; i++) + { + Dispose(project.Content.Folder.Children[i]); + i--; + } + } + if (project.Source != null) + { + //Dispose(project.Source.Folder); + for (int i = 0; i < project.Source.Folder.Children.Count; i++) + { + Dispose(project.Source.Folder.Children[i]); + i--; + } + } + } + + List removeProxies = new List(); + foreach (var proxy in Editor.Instance.ContentDatabase.Proxy) + { + if (proxy.GetType().IsCollectible) + removeProxies.Add(proxy); + } + foreach (var proxy in removeProxies) + RemoveProxy(proxy, false); + + _isDuringFastSetup = false; + _enableEvents = enabledEvents; + } + + private void OnScriptsReloadEnd() + { + RebuildInternal(); + } + /// public override void OnUpdate() { @@ -1340,6 +1398,8 @@ namespace FlaxEditor.Modules public override void OnExit() { FlaxEngine.Content.AssetDisposing -= OnContentAssetDisposing; + ScriptsBuilder.ScriptsReload -= OnScriptsReload; + ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd; // Disable events _enableEvents = false; diff --git a/Source/Editor/Modules/ContentImportingModule.cs b/Source/Editor/Modules/ContentImportingModule.cs index 65821cd43..d235a792e 100644 --- a/Source/Editor/Modules/ContentImportingModule.cs +++ b/Source/Editor/Modules/ContentImportingModule.cs @@ -391,6 +391,20 @@ namespace FlaxEditor.Modules public override void OnInit() { ImportFileEntry.RegisterDefaultTypes(); + ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; + } + + private void OnScriptsReloadBegin() + { + // Remove import file types from scripting assemblies + List removeFileTypes = new List(); + foreach (var pair in ImportFileEntry.FileTypes) + { + if (pair.Value.Method.IsCollectible || (pair.Value.Target != null && pair.Value.Target.GetType().IsCollectible)) + removeFileTypes.Add(pair.Key); + } + foreach (var fileType in removeFileTypes) + ImportFileEntry.FileTypes.Remove(fileType); } /// @@ -451,6 +465,7 @@ namespace FlaxEditor.Modules /// public override void OnExit() { + ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; EndWorker(); } } diff --git a/Source/Editor/Modules/SceneModule.cs b/Source/Editor/Modules/SceneModule.cs index 850e4df3d..0d37a464f 100644 --- a/Source/Editor/Modules/SceneModule.cs +++ b/Source/Editor/Modules/SceneModule.cs @@ -58,7 +58,7 @@ namespace FlaxEditor.Modules : base(editor) { // After editor cache but before the windows - InitOrder = -900; + InitOrder = -800; } /// diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index cbd94ef41..67df5a2e8 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -160,7 +160,7 @@ namespace FlaxEditor.Modules internal UIModule(Editor editor) : base(editor) { - InitOrder = -90; + InitOrder = -70; VisjectSurfaceBackground = FlaxEngine.Content.LoadAsyncInternal("Editor/VisjectSurface"); ColorValueBox.ShowPickColorDialog += ShowPickColorDialog; } diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index fa7a04d9b..6c4af3b2f 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -5,10 +5,12 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; +using System.Runtime.InteropServices; using System.Text; using System.Xml; using FlaxEditor.Content; using FlaxEditor.GUI.Dialogs; +using FlaxEditor.GUI.Docking; using FlaxEditor.Windows; using FlaxEditor.Windows.Assets; using FlaxEditor.Windows.Profiler; @@ -39,6 +41,7 @@ namespace FlaxEditor.Modules public DockState DockState; public DockPanel DockedTo; + public int DockedTabIndex; public float? SplitterValue = null; public bool SelectOnShow = false; @@ -48,6 +51,8 @@ namespace FlaxEditor.Modules public Float2 FloatSize; public Float2 FloatPosition; + public Guid AssetItemID; + // Constructor, to allow for default values public WindowRestoreData() { @@ -803,43 +808,64 @@ namespace FlaxEditor.Modules Level.SceneSaving += OnSceneSaving; Level.SceneUnloaded += OnSceneUnloaded; Level.SceneUnloading += OnSceneUnloading; - ScriptsBuilder.ScriptsReloadEnd += OnScriptsReloadEnd; + Editor.ContentDatabase.WorkspaceRebuilt += OnWorkspaceRebuilt; Editor.StateMachine.StateChanged += OnEditorStateChanged; } + internal void AddToRestore(AssetEditorWindow win) + { + AddToRestore(win, win.GetType(), new WindowRestoreData + { + AssetItemID = win.Item.ID, + }); + } + internal void AddToRestore(CustomEditorWindow win) { - var type = win.GetType(); + AddToRestore(win.Window, win.GetType(), new WindowRestoreData()); + } + private void AddToRestore(EditorWindow win, Type type, WindowRestoreData winData) + { // Validate if can restore type var constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); if (constructor == null || type.IsGenericType) return; - var winData = new WindowRestoreData(); - var panel = win.Window.ParentDockPanel; + var panel = win.ParentDockPanel; // Ensure that this window is only selected following recompilation // if it was the active tab in its dock panel. Otherwise, there is a // risk of interrupting the user's workflow by potentially selecting // background tabs. - winData.SelectOnShow = panel.SelectedTab == win.Window; - if (panel is FloatWindowDockPanel) + var window = win.RootWindow.Window; + winData.SelectOnShow = panel.SelectedTab == win; + winData.DockedTabIndex = 0; + if (panel is FloatWindowDockPanel && window != null && panel.TabsCount == 1) { winData.DockState = DockState.Float; - var window = win.Window.RootWindow.Window; winData.FloatPosition = window.Position; winData.FloatSize = window.ClientSize; winData.Maximize = window.IsMaximized; winData.Minimize = window.IsMinimized; + winData.DockedTo = panel; } else { + for (int i = 0; i < panel.Tabs.Count; i++) + { + if (panel.Tabs[i] == win) + { + winData.DockedTabIndex = i; + break; + } + } if (panel.TabsCount > 1) { winData.DockState = DockState.DockFill; winData.DockedTo = panel; - }else + } + else { winData.DockState = panel.TryGetDockState(out var splitterValue); winData.DockedTo = panel.ParentDockPanel; @@ -851,38 +877,93 @@ namespace FlaxEditor.Modules _restoreWindows.Add(winData); } - private void OnScriptsReloadEnd() + private void OnWorkspaceRebuilt() { - for (int i = 0; i < _restoreWindows.Count; i++) + // Go in reverse order to create floating Prefab windows first before docked windows + for (int i = _restoreWindows.Count - 1; i >= 0; i--) { var winData = _restoreWindows[i]; try { var assembly = Utils.GetAssemblyByName(winData.AssemblyName); - if (assembly != null) + if (assembly == null) + continue; + + var type = assembly.GetType(winData.TypeName); + if (type == null) + continue; + + if (type.IsAssignableTo(typeof(AssetEditorWindow))) { - var type = assembly.GetType(winData.TypeName); - if (type != null) + var ctor = type.GetConstructor(new Type[] { typeof(Editor), typeof(AssetItem) }); + var assetItem = Editor.ContentDatabase.FindAsset(winData.AssetItemID); + var win = (AssetEditorWindow)ctor.Invoke(new object[] { Editor.Instance, assetItem }); + win.Show(winData.DockState, winData.DockState != DockState.Float ? winData.DockedTo : null, winData.SelectOnShow, winData.SplitterValue); + if (winData.DockState == DockState.Float) { - var win = (CustomEditorWindow)Activator.CreateInstance(type); - win.Show(winData.DockState, winData.DockedTo, winData.SelectOnShow, winData.SplitterValue); - if (winData.DockState == DockState.Float) + var window = win.RootWindow.Window; + window.Position = winData.FloatPosition; + if (winData.Maximize) { - var window = win.Window.RootWindow.Window; - window.Position = winData.FloatPosition; - if (winData.Maximize) - { - window.Maximize(); - } - else if (winData.Minimize) - { - window.Minimize(); - } - else - { - window.ClientSize = winData.FloatSize; - } + window.Maximize(); + } + else if (winData.Minimize) + { + window.Minimize(); + } + else + { + window.ClientSize = winData.FloatSize; + } + + // Update panel reference in other windows docked to this panel + foreach (ref var otherData in CollectionsMarshal.AsSpan(_restoreWindows)) + { + if (otherData.DockedTo == winData.DockedTo) + otherData.DockedTo = win.ParentDockPanel; + } + } + var panel = win.ParentDockPanel; + int currentTabIndex = 0; + for (int pi = 0; pi < panel.TabsCount; pi++) + { + if (panel.Tabs[pi] == win) + { + currentTabIndex = pi; + break; + } + } + while (currentTabIndex > winData.DockedTabIndex) + { + win.ParentDockPanel.MoveTabLeft(currentTabIndex); + currentTabIndex--; + } + while (currentTabIndex < winData.DockedTabIndex) + { + win.ParentDockPanel.MoveTabRight(currentTabIndex); + currentTabIndex++; + } + } + else + { + var win = (CustomEditorWindow)Activator.CreateInstance(type); + win.Show(winData.DockState, winData.DockedTo, winData.SelectOnShow, winData.SplitterValue); + if (winData.DockState == DockState.Float) + { + var window = win.Window.RootWindow.Window; + window.Position = winData.FloatPosition; + if (winData.Maximize) + { + window.Maximize(); + } + else if (winData.Minimize) + { + window.Minimize(); + } + else + { + window.ClientSize = winData.FloatSize; } } } @@ -893,6 +974,11 @@ namespace FlaxEditor.Modules Editor.LogWarning(string.Format("Failed to restore window {0} (assembly: {1})", winData.TypeName, winData.AssemblyName)); } } + + // Restored windows stole the focus from Editor + if (_restoreWindows.Count > 0) + Editor.Instance.Windows.MainWindow.Focus(); + _restoreWindows.Clear(); } @@ -1048,7 +1134,7 @@ namespace FlaxEditor.Modules Level.SceneSaving -= OnSceneSaving; Level.SceneUnloaded -= OnSceneUnloaded; Level.SceneUnloading -= OnSceneUnloading; - ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd; + Editor.ContentDatabase.WorkspaceRebuilt -= OnWorkspaceRebuilt; Editor.StateMachine.StateChanged -= OnEditorStateChanged; // Close main window diff --git a/Source/Editor/SceneGraph/SceneGraphNode.cs b/Source/Editor/SceneGraph/SceneGraphNode.cs index 5becb69dc..37afc896e 100644 --- a/Source/Editor/SceneGraph/SceneGraphNode.cs +++ b/Source/Editor/SceneGraph/SceneGraphNode.cs @@ -469,6 +469,7 @@ namespace FlaxEditor.SceneGraph { ChildNodes[i].OnDispose(); } + ChildNodes.Clear(); SceneGraphFactory.Nodes.Remove(ID); } diff --git a/Source/Editor/Surface/AnimGraphSurface.cs b/Source/Editor/Surface/AnimGraphSurface.cs index c33224072..30a15d7f4 100644 --- a/Source/Editor/Surface/AnimGraphSurface.cs +++ b/Source/Editor/Surface/AnimGraphSurface.cs @@ -161,6 +161,8 @@ namespace FlaxEditor.Surface private void OnScriptsReloadBegin() { + _nodesCache.Clear(); + // Check if any of the nodes comes from the game scripts - those can be reloaded at runtime so prevent crashes bool hasTypeFromGameScripts = Editor.Instance.CodeEditing.AnimGraphNodes.HasTypeFromGameScripts; diff --git a/Source/Editor/Surface/BehaviorTreeSurface.cs b/Source/Editor/Surface/BehaviorTreeSurface.cs index 647c438e8..2dba081b0 100644 --- a/Source/Editor/Surface/BehaviorTreeSurface.cs +++ b/Source/Editor/Surface/BehaviorTreeSurface.cs @@ -24,6 +24,7 @@ namespace FlaxEditor.Surface public BehaviorTreeSurface(IVisjectSurfaceOwner owner, Action onSave, FlaxEditor.Undo undo) : base(owner, onSave, undo, CreateStyle()) { + ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; } private static SurfaceStyle CreateStyle() @@ -35,6 +36,11 @@ namespace FlaxEditor.Surface return style; } + private void OnScriptsReloadBegin() + { + _nodesCache.Clear(); + } + private static void DrawBox(Box box) { var rect = new Rectangle(Float2.Zero, box.Size); @@ -186,6 +192,7 @@ namespace FlaxEditor.Surface { if (IsDisposing) return; + ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; _nodesCache.Wait(); base.OnDestroy(); diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 28ec17d53..66572324d 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -415,6 +415,15 @@ namespace FlaxEditor.Surface // Init drag handlers DragHandlers.Add(_dragAssets = new DragAssets(ValidateDragItem)); DragHandlers.Add(_dragParameters = new DragNames(SurfaceParameter.DragPrefix, ValidateDragParameter)); + + ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; + } + + private void OnScriptsReloadBegin() + { + _activeVisjectCM = null; + _cmPrimaryMenu?.Dispose(); + _cmPrimaryMenu = null; } /// @@ -1023,6 +1032,8 @@ namespace FlaxEditor.Surface _activeVisjectCM = null; _cmPrimaryMenu?.Dispose(); + ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; + base.OnDestroy(); } } diff --git a/Source/Editor/Surface/VisualScriptSurface.cs b/Source/Editor/Surface/VisualScriptSurface.cs index ff0153e8a..672a527a9 100644 --- a/Source/Editor/Surface/VisualScriptSurface.cs +++ b/Source/Editor/Surface/VisualScriptSurface.cs @@ -62,6 +62,12 @@ namespace FlaxEditor.Surface { _supportsImplicitCastFromObjectToBoolean = true; DragHandlers.Add(_dragActors = new DragActors(ValidateDragActor)); + ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; + } + + private void OnScriptsReloadBegin() + { + _nodesCache.Clear(); } private bool ValidateDragActor(ActorNode actor) @@ -631,6 +637,7 @@ namespace FlaxEditor.Surface { if (IsDisposing) return; + ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; _nodesCache.Wait(); base.OnDestroy(); diff --git a/Source/Editor/Windows/Assets/AssetEditorWindow.cs b/Source/Editor/Windows/Assets/AssetEditorWindow.cs index f781d49dc..e97cb7371 100644 --- a/Source/Editor/Windows/Assets/AssetEditorWindow.cs +++ b/Source/Editor/Windows/Assets/AssetEditorWindow.cs @@ -58,6 +58,8 @@ namespace FlaxEditor.Windows.Assets InputActions.Add(options => options.Save, Save); UpdateTitle(); + + ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; } /// @@ -151,6 +153,8 @@ namespace FlaxEditor.Windows.Assets /// public override void OnDestroy() { + ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; + if (_item != null) { // Ensure to remove linkage to the item @@ -160,6 +164,15 @@ namespace FlaxEditor.Windows.Assets base.OnDestroy(); } + /// + protected virtual void OnScriptsReloadBegin() + { + if (!IsHidden) + { + Editor.Instance.Windows.AddToRestore(this); + } + } + #region IEditable Implementation private bool _isEdited; diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs index 3d6399365..212e0c82f 100644 --- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs +++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs @@ -268,8 +268,11 @@ namespace FlaxEditor.Windows.Assets UpdateKnowledge(); } - private void OnScriptsReloadBegin() + /// + protected override void OnScriptsReloadBegin() { + base.OnScriptsReloadBegin(); + // TODO: impl hot-reload for BT to nicely refresh state (save asset, clear undo/properties, reload surface) Close(); } diff --git a/Source/Editor/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs index 38acfbe15..520d99261 100644 --- a/Source/Editor/Windows/Assets/JsonAssetWindow.cs +++ b/Source/Editor/Windows/Assets/JsonAssetWindow.cs @@ -124,8 +124,10 @@ namespace FlaxEditor.Windows.Assets UpdateToolstrip(); } - private void OnScriptsReloadBegin() + /// + protected override void OnScriptsReloadBegin() { + base.OnScriptsReloadBegin(); Close(); } diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index 03d666478..1d1ccc50a 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -198,7 +198,6 @@ namespace FlaxEditor.Windows.Assets Editor.Prefabs.PrefabApplied += OnPrefabApplied; ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; - ScriptsBuilder.ScriptsReloadEnd += OnScriptsReloadEnd; // Setup input actions InputActions.Add(options => options.Undo, () => @@ -291,8 +290,10 @@ namespace FlaxEditor.Windows.Assets return false; } - private void OnScriptsReloadBegin() + /// + protected override void OnScriptsReloadBegin() { + base.OnScriptsReloadBegin(); _isScriptsReloading = true; if (_asset == null || !_asset.IsLoaded) @@ -320,19 +321,8 @@ namespace FlaxEditor.Windows.Assets Graph.MainActor = null; _viewport.Prefab = null; _undo?.Clear(); // TODO: maybe don't clear undo? - } - private void OnScriptsReloadEnd() - { - _isScriptsReloading = false; - - if (_asset == null || !_asset.IsLoaded) - return; - - // Restore - OnPrefabOpened(); - _undo.Clear(); - ClearEditedFlag(); + Close(); } private void OnUndoEvent(IUndoAction action) @@ -551,7 +541,6 @@ namespace FlaxEditor.Windows.Assets { Editor.Prefabs.PrefabApplied -= OnPrefabApplied; ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; - ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd; _undo.Dispose(); Graph.Dispose(); diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index 6089b2e07..f452c485a 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -29,6 +29,7 @@ namespace FlaxEditor.Windows private const string ProjectDataLastViewedFolder = "LastViewedFolder"; private bool _isWorkspaceDirty; private string _workspaceRebuildLocation; + private string _lastViewedFolderBeforeReload; private SplitPanel _split; private Panel _contentViewPanel; private Panel _contentTreePanel; @@ -144,26 +145,6 @@ namespace FlaxEditor.Windows FlaxEditor.Utilities.Utils.SetupCommonInputActions(this); - // Content database events - editor.ContentDatabase.WorkspaceModified += () => _isWorkspaceDirty = true; - editor.ContentDatabase.ItemRemoved += OnContentDatabaseItemRemoved; - editor.ContentDatabase.WorkspaceRebuilding += () => { _workspaceRebuildLocation = SelectedNode?.Path; }; - editor.ContentDatabase.WorkspaceRebuilt += () => - { - var selected = Editor.ContentDatabase.Find(_workspaceRebuildLocation); - if (selected is ContentFolder selectedFolder) - { - _navigationUnlocked = false; - RefreshView(selectedFolder.Node); - _tree.Select(selectedFolder.Node); - UpdateItemsSearch(); - _navigationUnlocked = true; - UpdateUI(); - } - else - ShowRoot(); - }; - var options = Editor.Options; options.OptionsChanged += OnOptionsChanged; @@ -1036,6 +1017,61 @@ namespace FlaxEditor.Windows /// public override void OnInit() + { + // Content database events + Editor.ContentDatabase.WorkspaceModified += () => _isWorkspaceDirty = true; + Editor.ContentDatabase.ItemRemoved += OnContentDatabaseItemRemoved; + Editor.ContentDatabase.WorkspaceRebuilding += () => { _workspaceRebuildLocation = SelectedNode?.Path; }; + Editor.ContentDatabase.WorkspaceRebuilt += () => + { + var selected = Editor.ContentDatabase.Find(_workspaceRebuildLocation); + if (selected is ContentFolder selectedFolder) + { + _navigationUnlocked = false; + RefreshView(selectedFolder.Node); + _tree.Select(selectedFolder.Node); + UpdateItemsSearch(); + _navigationUnlocked = true; + UpdateUI(); + } + else if (_root != null) + ShowRoot(); + }; + + Refresh(); + + // Load last viewed folder + if (Editor.ProjectCache.TryGetCustomData(ProjectDataLastViewedFolder, out string lastViewedFolder)) + { + if (Editor.ContentDatabase.Find(lastViewedFolder) is ContentFolder folder) + _tree.Select(folder.Node); + } + + ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; + ScriptsBuilder.ScriptsReloadEnd += OnScriptsReloadEnd; + } + + private void OnScriptsReloadBegin() + { + var lastViewedFolder = _tree.Selection.Count == 1 ? _tree.SelectedNode as ContentTreeNode : null; + _lastViewedFolderBeforeReload = lastViewedFolder?.Path ?? string.Empty; + + _tree.RemoveChild(_root); + _root = null; + } + + private void OnScriptsReloadEnd() + { + Refresh(); + + if (!string.IsNullOrEmpty(_lastViewedFolderBeforeReload)) + { + if (Editor.ContentDatabase.Find(_lastViewedFolderBeforeReload) is ContentFolder folder) + _tree.Select(folder.Node); + } + } + + private void Refresh() { // Setup content root node _root = new RootContentTreeNode @@ -1072,13 +1108,6 @@ namespace FlaxEditor.Windows // Update UI layout _isLayoutLocked = false; PerformLayout(); - - // Load last viewed folder - if (Editor.ProjectCache.TryGetCustomData(ProjectDataLastViewedFolder, out string lastViewedFolder)) - { - if (Editor.ContentDatabase.Find(lastViewedFolder) is ContentFolder folder) - _tree.Select(folder.Node); - } } /// @@ -1226,6 +1255,8 @@ namespace FlaxEditor.Windows _viewDropdown = null; Editor.Options.OptionsChanged -= OnOptionsChanged; + ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; + ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd; base.OnDestroy(); } diff --git a/Source/Editor/Windows/ToolboxWindow.cs b/Source/Editor/Windows/ToolboxWindow.cs index 4b347ea83..dd550b0f1 100644 --- a/Source/Editor/Windows/ToolboxWindow.cs +++ b/Source/Editor/Windows/ToolboxWindow.cs @@ -150,6 +150,22 @@ namespace FlaxEditor.Windows _searchBox.Clear(); _groupSearch.DisposeChildren(); _groupSearch.PerformLayout(); + + // Remove tabs + var tabs = new List(); + foreach (var child in _actorGroups.Children) + { + if (child is Tab tab) + { + if (tab.Text != "Search") + tabs.Add(tab); + } + } + foreach (var tab in tabs) + { + var group = _actorGroups.Children.Find(T => T == tab); + group.Dispose(); + } } private void OnScriptsReloadEnd() diff --git a/Source/Engine/Engine/NativeInterop.Managed.cs b/Source/Engine/Engine/NativeInterop.Managed.cs index c7507c778..148efb007 100644 --- a/Source/Engine/Engine/NativeInterop.Managed.cs +++ b/Source/Engine/Engine/NativeInterop.Managed.cs @@ -176,6 +176,7 @@ namespace FlaxEngine.Interop _managedHandle.Free(); _unmanagedData = IntPtr.Zero; } + _arrayType = _elementType = null; ManagedArrayPool.Put(this); } @@ -442,22 +443,25 @@ namespace FlaxEngine.Interop /// /// Tries to free all references to old weak handles so GC can collect them. /// - internal static void TryCollectWeakHandles() + internal static void TryCollectWeakHandles(bool force = false) { - if (weakHandleAccumulator < nextWeakPoolCollection) - return; + if (!force) + { + if (weakHandleAccumulator < nextWeakPoolCollection) + return; - nextWeakPoolCollection = weakHandleAccumulator + 1000; + nextWeakPoolCollection = weakHandleAccumulator + 1000; - // Try to swap pools after garbage collection or whenever the pool gets too large - var gc0CollectionCount = GC.CollectionCount(0); - if (gc0CollectionCount < nextWeakPoolGCCollection && weakPool.Count < WeakPoolCollectionSizeThreshold) - return; - nextWeakPoolGCCollection = gc0CollectionCount + 1; + // Try to swap pools after garbage collection or whenever the pool gets too large + var gc0CollectionCount = GC.CollectionCount(0); + if (gc0CollectionCount < nextWeakPoolGCCollection && weakPool.Count < WeakPoolCollectionSizeThreshold) + return; + nextWeakPoolGCCollection = gc0CollectionCount + 1; - // Prevent huge allocations from swapping the pools in the middle of the operation - if (System.Diagnostics.Stopwatch.GetElapsedTime(lastWeakPoolCollectionTime).TotalMilliseconds < WeakPoolCollectionTimeThreshold) - return; + // Prevent huge allocations from swapping the pools in the middle of the operation + if (System.Diagnostics.Stopwatch.GetElapsedTime(lastWeakPoolCollectionTime).TotalMilliseconds < WeakPoolCollectionTimeThreshold) + return; + } lastWeakPoolCollectionTime = System.Diagnostics.Stopwatch.GetTimestamp(); // Swap the pools and release the oldest pool for GC diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index 71caee1e9..5a437dad2 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -1054,7 +1054,49 @@ namespace FlaxEngine.Interop } [UnmanagedCallersOnly] - internal static void ReloadScriptingAssemblyLoadContext() + internal static void CreateScriptingAssemblyLoadContext() + { +#if FLAX_EDITOR + if (scriptingAssemblyLoadContext != null) + { + // Wait for previous ALC to finish unloading, track it without holding strong references to it + GCHandle weakRef = GCHandle.Alloc(scriptingAssemblyLoadContext, GCHandleType.WeakTrackResurrection); + scriptingAssemblyLoadContext = null; +#if false + // In case the ALC doesn't unload properly: https://learn.microsoft.com/en-us/dotnet/standard/assembly/unloadability#debug-unloading-issues + while (true) +#else + for (int attempts = 5; attempts > 0; attempts--) +#endif + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + + if (!IsHandleAlive(weakRef)) + break; + System.Threading.Thread.Sleep(1); + } + if (IsHandleAlive(weakRef)) + Debug.Logger.LogHandler.LogWrite(LogType.Warning, "Scripting AssemblyLoadContext was not unloaded."); + weakRef.Free(); + + static bool IsHandleAlive(GCHandle weakRef) + { + // Checking the target in scope somehow holds a reference to it...? + return weakRef.Target != null; + } + } + + scriptingAssemblyLoadContext = new AssemblyLoadContext("Flax", isCollectible: true); + scriptingAssemblyLoadContext.Resolving += OnScriptingAssemblyLoadContextResolving; +#else + scriptingAssemblyLoadContext = new AssemblyLoadContext("Flax", isCollectible: false); +#endif + DelegateHelpers.InitMethods(); + } + + [UnmanagedCallersOnly] + internal static void UnloadScriptingAssemblyLoadContext() { #if FLAX_EDITOR // Clear all caches which might hold references to assemblies in collectible ALC @@ -1072,24 +1114,86 @@ namespace FlaxEngine.Interop handle.Free(); propertyHandleCacheCollectible.Clear(); + foreach (var key in assemblyHandles.Keys.Where(x => x.IsCollectible)) + assemblyHandles.Remove(key); + foreach (var key in assemblyOwnedNativeLibraries.Keys.Where(x => x.IsCollectible)) + assemblyOwnedNativeLibraries.Remove(key); + _typeSizeCache.Clear(); foreach (var pair in classAttributesCacheCollectible) pair.Value.Free(); classAttributesCacheCollectible.Clear(); + ArrayFactory.marshalledTypes.Clear(); + ArrayFactory.arrayTypes.Clear(); + ArrayFactory.createArrayDelegates.Clear(); + FlaxEngine.Json.JsonSerializer.ResetCache(); + DelegateHelpers.Release(); + + // Ensure both pools are empty + ManagedHandle.ManagedHandlePool.TryCollectWeakHandles(true); + ManagedHandle.ManagedHandlePool.TryCollectWeakHandles(true); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + { + // HACK: Workaround for TypeDescriptor holding references to collectible types (https://github.com/dotnet/runtime/issues/30656) + + Type typeDescriptionProviderType = typeof(System.ComponentModel.TypeDescriptionProvider); + MethodInfo clearCacheMethod = typeDescriptionProviderType?.Assembly.GetType("System.ComponentModel.ReflectionCachesUpdateHandler")?.GetMethod("ClearCache"); + if (clearCacheMethod != null) + clearCacheMethod.Invoke(null, new object[] { null }); + + Type TypeDescriptorType = typeof(System.ComponentModel.TypeDescriptor); + object s_providerTable = TypeDescriptorType?.GetField("s_providerTable", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)?.GetValue(null); + + // Added in .NET runtime 8.0.10, used as the main locking object + object s_commonSyncObject = TypeDescriptorType?.GetField("s_commonSyncObject", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)?.GetValue(null); + if (s_commonSyncObject == null) + s_commonSyncObject = s_providerTable; + + // Removed in .NET runtime 8.0.7 + object s_internalSyncObject = TypeDescriptorType?.GetField("s_internalSyncObject", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)?.GetValue(null); + object s_defaultProviders = TypeDescriptorType?.GetField("s_defaultProviders", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)?.GetValue(null); + if (s_internalSyncObject != null && s_defaultProviders != null) + { + lock (s_internalSyncObject) + InvokeClear(s_defaultProviders); + } + + // Replaces s_defaultProviders in 8.0.7 + object s_defaultProviderInitialized = TypeDescriptorType?.GetField("s_defaultProviderInitialized", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)?.GetValue(null); + if (s_commonSyncObject != null && s_defaultProviderInitialized != null) + { + lock (s_commonSyncObject) + InvokeClear(s_defaultProviderInitialized); + } + + object s_providerTypeTable = TypeDescriptorType?.GetField("s_providerTypeTable", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)?.GetValue(null); + if (s_providerTable != null && s_providerTypeTable != null) + { + lock (s_commonSyncObject) + InvokeClear(s_providerTypeTable); + InvokeClear(s_providerTable); + } + + static void InvokeClear(object instance) + { + Type type = instance.GetType(); + Assertions.Assert.IsTrue(type.Name == "ConcurrentDictionary`2" || type.Name == "Hashtable" || type.Name == "WeakHashtable"); + type.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(x => x.Name == "Clear")?.Invoke(instance, Array.Empty()); + } + } // Unload the ALC - bool unloading = true; - scriptingAssemblyLoadContext.Unloading += (alc) => { unloading = false; }; scriptingAssemblyLoadContext.Unload(); + scriptingAssemblyLoadContext.Resolving -= OnScriptingAssemblyLoadContextResolving; - while (unloading) - System.Threading.Thread.Sleep(1); - - InitScriptingAssemblyLoadContext(); - DelegateHelpers.InitMethods(); + GC.Collect(); + GC.WaitForPendingFinalizers(); #endif } diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index 35fe70221..7a9a8e5ff 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -73,19 +73,6 @@ namespace FlaxEngine.Interop return nativeLibrary; } - private static void InitScriptingAssemblyLoadContext() - { -#if FLAX_EDITOR - var isCollectible = true; -#else - var isCollectible = false; -#endif - scriptingAssemblyLoadContext = new AssemblyLoadContext("Flax", isCollectible); -#if FLAX_EDITOR - scriptingAssemblyLoadContext.Resolving += OnScriptingAssemblyLoadContextResolving; -#endif - } - [UnmanagedCallersOnly] internal static unsafe void Init() { @@ -97,8 +84,6 @@ namespace FlaxEngine.Interop System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - InitScriptingAssemblyLoadContext(); - DelegateHelpers.InitMethods(); } #if FLAX_EDITOR @@ -1475,11 +1460,11 @@ namespace FlaxEngine.Interop internal static class ArrayFactory { - private delegate Array CreateArrayDelegate(long size); + internal delegate Array CreateArrayDelegate(long size); - private static ConcurrentDictionary marshalledTypes = new ConcurrentDictionary(1, 3); - private static ConcurrentDictionary arrayTypes = new ConcurrentDictionary(1, 3); - private static ConcurrentDictionary createArrayDelegates = new ConcurrentDictionary(1, 3); + internal static ConcurrentDictionary marshalledTypes = new ConcurrentDictionary(1, 3); + internal static ConcurrentDictionary arrayTypes = new ConcurrentDictionary(1, 3); + internal static ConcurrentDictionary createArrayDelegates = new ConcurrentDictionary(1, 3); internal static Type GetMarshalledType(Type elementType) { @@ -1645,17 +1630,6 @@ namespace FlaxEngine.Interop return RegisterType(type, true).typeHolder; } - internal static (TypeHolder typeHolder, ManagedHandle handle) GetTypeHolderAndManagedHandle(Type type) - { - if (managedTypes.TryGetValue(type, out (TypeHolder typeHolder, ManagedHandle handle) tuple)) - return tuple; -#if FLAX_EDITOR - if (managedTypesCollectible.TryGetValue(type, out tuple)) - return tuple; -#endif - return RegisterType(type, true); - } - /// /// Returns a static ManagedHandle to TypeHolder for given Type, and caches it if needed. /// @@ -1781,6 +1755,14 @@ namespace FlaxEngine.Interop #endif } + internal static void Release() + { + MakeNewCustomDelegateFunc = null; +#if FLAX_EDITOR + MakeNewCustomDelegateFuncCollectible = null; +#endif + } + internal static Type MakeNewCustomDelegate(Type[] parameters) { #if FLAX_EDITOR diff --git a/Source/Engine/Scripting/ManagedCLR/MCore.h b/Source/Engine/Scripting/ManagedCLR/MCore.h index 35227df56..d975f5af7 100644 --- a/Source/Engine/Scripting/ManagedCLR/MCore.h +++ b/Source/Engine/Scripting/ManagedCLR/MCore.h @@ -45,9 +45,16 @@ public: /// static void UnloadEngine(); + /// + /// Creates the assembly load context for assemblies used by Scripting. + /// + static void CreateScriptingAssemblyLoadContext(); + #if USE_EDITOR - // Called by Scripting in a middle of hot-reload (after unloading modules but before loading them again). - static void ReloadScriptingAssemblyLoadContext(); + /// + /// Called by Scripting in a middle of hot-reload (after unloading modules but before loading them again). + /// + static void UnloadScriptingAssemblyLoadContext(); #endif public: diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index ddafab614..29ccea8ae 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -330,9 +330,15 @@ void MCore::UnloadEngine() ShutdownHostfxr(); } +void MCore::CreateScriptingAssemblyLoadContext() +{ + static void* CreateScriptingAssemblyLoadContextPtr = GetStaticMethodPointer(TEXT("CreateScriptingAssemblyLoadContext")); + CallStaticMethod(CreateScriptingAssemblyLoadContextPtr); +} + #if USE_EDITOR -void MCore::ReloadScriptingAssemblyLoadContext() +void MCore::UnloadScriptingAssemblyLoadContext() { // Clear any cached class attributes (see https://github.com/FlaxEngine/FlaxEngine/issues/1108) for (auto e : CachedClassHandles) @@ -377,8 +383,8 @@ void MCore::ReloadScriptingAssemblyLoadContext() } } - static void* ReloadScriptingAssemblyLoadContextPtr = GetStaticMethodPointer(TEXT("ReloadScriptingAssemblyLoadContext")); - CallStaticMethod(ReloadScriptingAssemblyLoadContextPtr); + static void* UnloadScriptingAssemblyLoadContextPtr = GetStaticMethodPointer(TEXT("UnloadScriptingAssemblyLoadContext")); + CallStaticMethod(UnloadScriptingAssemblyLoadContextPtr); } #endif diff --git a/Source/Engine/Scripting/Runtime/Mono.cpp b/Source/Engine/Scripting/Runtime/Mono.cpp index 351e1569b..f4da8135c 100644 --- a/Source/Engine/Scripting/Runtime/Mono.cpp +++ b/Source/Engine/Scripting/Runtime/Mono.cpp @@ -715,9 +715,13 @@ void MCore::UnloadEngine() #endif } +void MCore::CreateScriptingAssemblyLoadContext() +{ +} + #if USE_EDITOR -void MCore::ReloadScriptingAssemblyLoadContext() +void MCore::UnloadScriptingAssemblyLoadContext() { } diff --git a/Source/Engine/Scripting/Runtime/None.cpp b/Source/Engine/Scripting/Runtime/None.cpp index 8731ed00d..98bf1f012 100644 --- a/Source/Engine/Scripting/Runtime/None.cpp +++ b/Source/Engine/Scripting/Runtime/None.cpp @@ -58,9 +58,13 @@ void MCore::UnloadEngine() MRootDomain = nullptr; } +void MCore::CreateScriptingAssemblyLoadContext() +{ +} + #if USE_EDITOR -void MCore::ReloadScriptingAssemblyLoadContext() +void MCore::UnloadScriptingAssemblyLoadContext() { } diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp index a0b55376f..953118fca 100644 --- a/Source/Engine/Scripting/Scripting.cpp +++ b/Source/Engine/Scripting/Scripting.cpp @@ -182,6 +182,8 @@ bool ScriptingService::Init() return true; } + MCore::CreateScriptingAssemblyLoadContext(); + // Cache root domain _rootDomain = MCore::GetRootDomain(); @@ -710,7 +712,8 @@ void Scripting::Reload(bool canTriggerSceneReload) _hasGameModulesLoaded = false; // Release and create a new assembly load context for user assemblies - MCore::ReloadScriptingAssemblyLoadContext(); + MCore::UnloadScriptingAssemblyLoadContext(); + MCore::CreateScriptingAssemblyLoadContext(); // Give GC a try to cleanup old user objects and the other mess MCore::GC::Collect(); diff --git a/Source/Engine/Scripting/Scripting.cs b/Source/Engine/Scripting/Scripting.cs index 66a34faf0..33aea8a26 100644 --- a/Source/Engine/Scripting/Scripting.cs +++ b/Source/Engine/Scripting/Scripting.cs @@ -170,6 +170,10 @@ namespace FlaxEngine AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; Localization.LocalizationChanged += OnLocalizationChanged; +#if FLAX_EDITOR + FlaxEditor.ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; + FlaxEditor.ScriptsBuilder.ScriptsReloadEnd += OnScriptsReloadEnd; +#endif OnLocalizationChanged(); if (!Engine.IsEditor) @@ -178,6 +182,19 @@ namespace FlaxEngine } } +#if FLAX_EDITOR + private static void OnScriptsReloadBegin() + { + // Tooltip might hold references to scripting assemblies + Style.Current.SharedTooltip = null; + } + + private static void OnScriptsReloadEnd() + { + Style.Current.SharedTooltip = new Tooltip(); + } +#endif + private static void OnLocalizationChanged() { // Invariant-globalization only (see InitHostfxr with Mono) @@ -359,6 +376,10 @@ namespace FlaxEngine MainThreadTaskScheduler.Dispose(); Json.JsonSerializer.Dispose(); +#if FLAX_EDITOR + FlaxEditor.ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; + FlaxEditor.ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd; +#endif } /// diff --git a/Source/Engine/UI/GUI/Tooltip.cs b/Source/Engine/UI/GUI/Tooltip.cs index 40aae0067..8c8e05b43 100644 --- a/Source/Engine/UI/GUI/Tooltip.cs +++ b/Source/Engine/UI/GUI/Tooltip.cs @@ -120,6 +120,7 @@ namespace FlaxEngine.GUI // Unlink IsLayoutLocked = true; Parent = null; + _showTarget = null; // Close window if (_window) diff --git a/Source/Tools/Flax.Build/ProjectInfo.cs b/Source/Tools/Flax.Build/ProjectInfo.cs index c64627e3c..2c3a600dd 100644 --- a/Source/Tools/Flax.Build/ProjectInfo.cs +++ b/Source/Tools/Flax.Build/ProjectInfo.cs @@ -260,7 +260,11 @@ namespace Flax.Build return true; var rules = Builder.GenerateRulesAssembly(); var target = rules.GetTarget(name); - return target == null || target.Modules.TrueForAll(x => !rules.GetModule(x).BuildNativeCode); + return target == null || target.Modules.TrueForAll(moduleName => + { + var module = rules.GetModule(moduleName); + return module != null && !module.BuildNativeCode; + }); } ///