// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using FlaxEditor.Content; using FlaxEditor.Content.Settings; using FlaxEditor.Scripting; using FlaxEngine; namespace FlaxEditor.Modules { /// /// Manages assets database and searches for workspace directory changes. /// /// public sealed class ContentDatabaseModule : EditorModule { private bool _enableEvents; private bool _isDuringFastSetup; private int _itemsCreated; private int _itemsDeleted; private readonly HashSet _dirtyNodes = new HashSet(); /// /// The project directory. /// public ProjectTreeNode Game { get; private set; } /// /// The engine directory. /// public ProjectTreeNode Engine { get; private set; } /// /// The list of all projects workspace directories (including game, engine and plugins projects). /// public readonly List Projects = new List(); /// /// The list with all content items proxy objects. /// public readonly List Proxy = new List(64); /// /// Occurs when new items is added to the workspace content database. /// public event Action ItemAdded; /// /// Occurs when new items is removed from the workspace content database. /// public event Action ItemRemoved; /// /// Occurs when workspace has been modified. /// public event Action WorkspaceModified; /// /// Gets the amount of created items. /// public int ItemsCreated => _itemsCreated; /// /// Gets the amount of deleted items. /// public int ItemsDeleted => _itemsDeleted; internal ContentDatabaseModule(Editor editor) : base(editor) { // Init content database after UI module InitOrder = -80; // Register AssetItems serialization helper (serialize ref ID only) FlaxEngine.Json.JsonSerializer.Settings.Converters.Add(new AssetItemConverter()); } private void OnContentAssetDisposing(Asset asset) { // Handle deleted asset if (asset.ShouldDeleteFileOnUnload) { var item = Find(asset.ID); if (item != null) { // Close all asset editors Editor.Windows.CloseAllEditors(item); // Dispose item.Dispose(); } } } /// /// Gets the project workspace used by the given project. /// /// The project. /// The project workspace or null if not loaded into database. public ProjectTreeNode GetProjectWorkspace(ProjectInfo project) { return Projects.FirstOrDefault(x => x.Project == project); } /// /// Gets the proxy object for the given content item. /// /// The item. /// Content proxy for that item or null if cannot find. public ContentProxy GetProxy(ContentItem item) { if (item != null) { for (int i = 0; i < Proxy.Count; i++) { if (Proxy[i].IsProxyFor(item)) { return Proxy[i]; } } } return null; } /// /// Gets the proxy object for the given asset type. /// /// Content proxy for that asset type or null if cannot find. public ContentProxy GetProxy() where T : Asset { for (int i = 0; i < Proxy.Count; i++) { if (Proxy[i].IsProxyFor()) { return Proxy[i]; } } return null; } /// /// Gets the proxy object for the given file extension. Warning! Different asset types may share the same file extension. /// /// The file extension. /// Content proxy for that item or null if cannot find. public ContentProxy GetProxy(string extension) { if (string.IsNullOrEmpty(extension)) throw new ArgumentNullException(); extension = StringUtils.NormalizeExtension(extension); for (int i = 0; i < Proxy.Count; i++) { if (Proxy[i].FileExtension == extension) { return Proxy[i]; } } return null; } /// /// Gets the proxy object for the given asset type id. /// /// The asset type name. /// The asset path. /// Asset proxy or null if cannot find. public AssetProxy GetAssetProxy(string typeName, string path) { for (int i = 0; i < Proxy.Count; i++) { if (Proxy[i] is AssetProxy proxy && proxy.AcceptsAsset(typeName, path)) { return proxy; } } return null; } /// /// Refreshes the given item folder. Tries to find new content items and remove not existing ones. /// /// Folder to refresh /// True if search for changes inside a subdirectories, otherwise only top-most folder will be updated public void RefreshFolder(ContentItem item, bool checkSubDirs) { // Peek folder to refresh ContentFolder folder = item.IsFolder ? item as ContentFolder : item.ParentFolder; if (folder == null) return; // Update LoadFolder(folder.Node, checkSubDirs); } /// /// Tries to find item at the specified path. /// /// The path. /// Found item or null if cannot find it. public ContentItem Find(string path) { if (string.IsNullOrEmpty(path)) return null; // Ensure path is normalized to the Flax format path = StringUtils.NormalizePath(path); // TODO: if it's a bottleneck try to optimize searching by spiting path foreach (var project in Projects) { var result = project.Folder.Find(path); if (result != null) return result; } return null; } /// /// Tries to find item with the specified ID. /// /// The item ID. /// Found item or null if cannot find it. public ContentItem Find(Guid id) { if (id == Guid.Empty) return null; // TODO: use AssetInfo via Content manager to get asset path very quickly (it's O(1)) // TODO: if it's a bottleneck try to optimize searching by caching items IDs foreach (var project in Projects) { var result = project.Folder.Find(id); if (result != null) return result; } return null; } /// /// Tries to find asset with the specified ID. /// /// The asset ID. /// Found asset item or null if cannot find it. public AssetItem FindAsset(Guid id) { if (id == Guid.Empty) return null; // TODO: use AssetInfo via Content manager to get asset path very quickly (it's O(1)) // TODO: if it's a bottleneck try to optimize searching by caching items IDs foreach (var project in Projects) { if (project.Content?.Folder.Find(id) is AssetItem result) return result; } return null; } /// /// Tries to find script item at the specified path. /// /// The path. /// Found script or null if cannot find it. public ScriptItem FindScript(string path) { foreach (var project in Projects) { if (project.Source?.Folder.Find(path) is ScriptItem result) return result; } return null; } /// /// Tries to find script item with the specified ID. /// /// The item ID. /// Found script or null if cannot find it. public ScriptItem FindScript(Guid id) { if (id == Guid.Empty) return null; foreach (var project in Projects) { if (project.Source?.Folder.Find(id) is ScriptItem result) return result; } return null; } /// /// Tries to find script item with the specified name. /// /// The name of the script. /// Found script or null if cannot find it. public ScriptItem FindScriptWitScriptName(string scriptName) { foreach (var project in Projects) { if (project.Source?.Folder.FindScriptWitScriptName(scriptName) is ScriptItem result) return result; } return null; } /// /// Tries to find script item that is used by the specified script object. /// /// The instance of the script. /// Found script or null if cannot find it. public ScriptItem FindScriptWitScriptName(Script script) { return FindScriptWitScriptName(TypeUtils.GetObjectType(script)); } /// /// Tries to find script item that is used by the specified script type. /// /// The type of the script. /// Found script or null if cannot find it. public ScriptItem FindScriptWitScriptName(ScriptType scriptType) { if (scriptType != ScriptType.Null) { var className = scriptType.Name; var scriptName = ScriptItem.CreateScriptName(className); return FindScriptWitScriptName(scriptName); } return null; } /// /// Renames a content item /// /// Content item /// New path /// True if failed, otherwise false private static bool RenameAsset(ContentItem el, ref string newPath) { string oldPath = el.Path; // Check if use content pool if (el.IsAsset) { // Rename asset // Note: we use content backend because file may be in use or sth, it's safe if (FlaxEngine.Content.RenameAsset(oldPath, newPath)) { // Error Editor.LogError(string.Format("Cannot rename asset \'{0}\' to \'{1}\'", oldPath, newPath)); return true; } } else { // Rename file try { File.Move(oldPath, newPath); } catch (Exception ex) { // Error Editor.LogWarning(ex); Editor.LogError(string.Format("Cannot rename asset \'{0}\' to \'{1}\'", oldPath, newPath)); return true; } } // Change path el.UpdatePath(newPath); return false; } private static void UpdateAssetNewNameTree(ContentItem el) { string extension = Path.GetExtension(el.Path); string newPath = StringUtils.CombinePaths(el.ParentFolder.Path, el.ShortName + extension); // Special case for folders if (el.IsFolder) { // Cache data string oldPath = el.Path; var folder = (ContentFolder)el; // Create new folder try { Directory.CreateDirectory(newPath); } catch (Exception ex) { // Error Editor.LogWarning(ex); Editor.LogError(string.Format("Cannot move folder \'{0}\' to \'{1}\'", oldPath, newPath)); return; } // Change path el.UpdatePath(newPath); // Rename all child elements for (int i = 0; i < folder.Children.Count; i++) UpdateAssetNewNameTree(folder.Children[i]); } else { RenameAsset(el, ref newPath); } } /// /// Moves the specified items to the different location. Handles moving whole directories and single assets. /// /// The items. /// The new parent. public void Move(List items, ContentFolder newParent) { for (int i = 0; i < items.Count; i++) Move(items[i], newParent); } /// /// Moves the specified item to the different location. Handles moving whole directories and single assets. /// /// The item. /// The new parent. public void Move(ContentItem item, ContentFolder newParent) { if (newParent == null || item == null) throw new ArgumentNullException(); // Skip nothing to change if (item.ParentFolder == newParent) return; var extension = Path.GetExtension(item.Path); var newPath = StringUtils.CombinePaths(newParent.Path, item.ShortName + extension); Move(item, newPath); } /// /// Moves the specified item to the different location. Handles moving whole directories and single assets. /// /// The item. /// The new path. public void Move(ContentItem item, string newPath) { if (item == null || string.IsNullOrEmpty(newPath)) throw new ArgumentNullException(); if (item.IsFolder && Directory.Exists(newPath)) { // Error MessageBox.Show("Cannot move folder. Target location already exists."); return; } // Find target parent var newDirPath = Path.GetDirectoryName(newPath); var newParent = Find(newDirPath) as ContentFolder; if (newParent == null) { // Error MessageBox.Show("Cannot move item. Missing target location."); return; } // Perform renaming { string oldPath = item.Path; // Special case for folders if (item.IsFolder) { // Cache data var folder = (ContentFolder)item; // Create new folder try { Directory.CreateDirectory(newPath); } catch (Exception ex) { // Error Editor.LogWarning(ex); Editor.LogError(string.Format("Cannot move folder \'{0}\' to \'{1}\'", oldPath, newPath)); return; } // Change path item.UpdatePath(newPath); // Rename all child elements for (int i = 0; i < folder.Children.Count; i++) UpdateAssetNewNameTree(folder.Children[i]); // Delete old folder try { Directory.Delete(oldPath, true); } catch (Exception ex) { // Error Editor.LogWarning(ex); Editor.LogWarning(string.Format("Cannot remove folder \'{0}\'", oldPath)); return; } } else { if (RenameAsset(item, ref newPath)) { MessageBox.Show("Cannot rename item."); return; } } if (item.ParentFolder != null) item.ParentFolder.Node.SortChildren(); } // Link item item.ParentFolder = newParent; if (_enableEvents) WorkspaceModified?.Invoke(); } /// /// Copies the specified item to the target location. Handles copying whole directories and single assets. /// /// The item. /// The target item path. public void Copy(ContentItem item, string targetPath) { if (item == null || !item.Exists) { // Error MessageBox.Show("Cannot move item. It's missing."); return; } // Perform copy { string sourcePath = item.Path; // Special case for folders if (item.IsFolder) { // Cache data var folder = (ContentFolder)item; // Create new folder if missing if (!Directory.Exists(targetPath)) { try { Directory.CreateDirectory(targetPath); } catch (Exception ex) { // Error Editor.LogWarning(ex); Editor.LogError(string.Format("Cannot copy folder \'{0}\' to \'{1}\'", sourcePath, targetPath)); return; } } // Copy all child elements for (int i = 0; i < folder.Children.Count; i++) { var child = folder.Children[i]; var childExtension = Path.GetExtension(child.Path); var childTargetPath = StringUtils.CombinePaths(targetPath, child.ShortName + childExtension); Copy(folder.Children[i], childTargetPath); } } else { // Check if use content pool if (item.IsAsset || item.ItemType == ContentItemType.Scene) { // Rename asset // Note: we use content backend because file may be in use or sth, it's safe if (Editor.ContentEditing.CloneAssetFile(sourcePath, targetPath, Guid.NewGuid())) { // Error Editor.LogError(string.Format("Cannot copy asset \'{0}\' to \'{1}\'", sourcePath, targetPath)); return; } } else { // Copy file try { File.Copy(sourcePath, targetPath, true); } catch (Exception ex) { // Error Editor.LogWarning(ex); Editor.LogError(string.Format("Cannot copy asset \'{0}\' to \'{1}\'", sourcePath, targetPath)); return; } } } } } /// /// Deletes the specified item. /// /// The item. public void Delete(ContentItem item) { if (item == null) throw new ArgumentNullException(); // Fire events if (_enableEvents) ItemRemoved?.Invoke(item); item.OnDelete(); _itemsDeleted++; var path = item.Path; // Special case for folders if (item is ContentFolder folder) { // TODO: maybe don't remove folders recursive but at once? // Delete all children if (folder.Children.Count > 0) { var children = folder.Children.ToArray(); for (int i = 0; i < children.Length; i++) { Delete(children[0]); } } // Remove directory if (Directory.Exists(path)) { try { Directory.Delete(path, true); } catch (Exception ex) { // Error Editor.LogWarning(ex); Editor.LogWarning(string.Format("Cannot remove folder \'{0}\'", path)); return; } } // Unlink from the parent item.ParentFolder = null; // Delete tree node folder.Node.Dispose(); } else { // Check if it's an asset if (item.IsAsset) { // Delete asset by using content pool FlaxEngine.Content.DeleteAsset(path); } else { // Delete file if (File.Exists(path)) File.Delete(path); } // Unlink from the parent item.ParentFolder = null; // Delete item item.Dispose(); } if (_enableEvents) WorkspaceModified?.Invoke(); } private void LoadFolder(ContentTreeNode node, bool checkSubDirs) { if (node == null) return; // Temporary data var folder = node.Folder; var path = folder.Path; // Check for missing files/folders (skip it during fast tree setup) if (!_isDuringFastSetup) { for (int i = 0; i < folder.Children.Count; i++) { var child = folder.Children[i]; if (!child.Exists) { // Send info Editor.Log(string.Format($"Content item \'{child.Path}\' has been removed")); // Destroy it Delete(child); i--; } } } // Find files var files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly); if (node.CanHaveAssets) { LoadAssets(node, files); } if (node.CanHaveScripts) { LoadScripts(node, files); } // Get child directories var childFolders = Directory.GetDirectories(path); // Load child folders bool sortChildren = false; for (int i = 0; i < childFolders.Length; i++) { var childPath = StringUtils.NormalizePath(childFolders[i]); // Check if node already has that element (skip during init when we want to walk project dir very fast) ContentFolder childFolderNode = _isDuringFastSetup ? null : node.Folder.FindChild(childPath) as ContentFolder; if (childFolderNode == null) { // Create node ContentTreeNode n = new ContentTreeNode(node, childPath); if (!_isDuringFastSetup) sortChildren = true; // Load child folder LoadFolder(n, true); // Fire event if (_enableEvents) { ItemAdded?.Invoke(n.Folder); WorkspaceModified?.Invoke(); } _itemsCreated++; } else if (checkSubDirs) { // Update child folder LoadFolder(childFolderNode.Node, true); } } if (sortChildren) node.SortChildren(); } private void LoadScripts(ContentTreeNode parent, string[] files) { for (int i = 0; i < files.Length; i++) { var path = StringUtils.NormalizePath(files[i]); // Check if node already has that element (skip during init when we want to walk project dir very fast) if (_isDuringFastSetup || !parent.Folder.ContainsChild(path)) { // Create file item ContentItem item; if (path.EndsWith(".cs")) item = new CSharpScriptItem(path); else if (path.EndsWith(".cpp") || path.EndsWith(".h")) item = new CppScriptItem(path); else if (path.EndsWith(".shader") || path.EndsWith(".hlsl")) item = new ShaderSourceItem(path); else item = new FileItem(path); // Link item.ParentFolder = parent.Folder; // Fire event if (_enableEvents) { ItemAdded?.Invoke(item); WorkspaceModified?.Invoke(); if (!path.EndsWith(".Gen.cs")) { if (item is ScriptItem) ScriptsBuilder.MarkWorkspaceDirty(); if (item is ScriptItem || item is ShaderSourceItem) Editor.CodeEditing.SelectedEditor.OnFileAdded(path); } } _itemsCreated++; } } } private void LoadAssets(ContentTreeNode parent, string[] files) { for (int i = 0; i < files.Length; i++) { var path = StringUtils.NormalizePath(files[i]); // Check if node already has that element (skip during init when we want to walk project dir very fast) if (_isDuringFastSetup || !parent.Folder.ContainsChild(path)) { // Create file item ContentItem item = null; if (FlaxEngine.Content.GetAssetInfo(path, out var assetInfo)) { var proxy = GetAssetProxy(assetInfo.TypeName, path); item = proxy?.ConstructItem(path, assetInfo.TypeName, ref assetInfo.ID); } if (item == null) item = new FileItem(path); // Link item.ParentFolder = parent.Folder; // Fire event if (_enableEvents) { ItemAdded?.Invoke(item); WorkspaceModified?.Invoke(); } _itemsCreated++; } } } private void LoadProjects(ProjectInfo project) { var workspace = GetProjectWorkspace(project); if (workspace == null) { workspace = new ProjectTreeNode(project); Projects.Add(workspace); var contentFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Content"); if (Directory.Exists(contentFolder)) { workspace.Content = new MainContentTreeNode(workspace, ContentFolderType.Content, contentFolder); workspace.Content.Folder.ParentFolder = workspace.Folder; } var sourceFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Source"); if (Directory.Exists(sourceFolder)) { workspace.Source = new MainContentTreeNode(workspace, ContentFolderType.Source, sourceFolder); workspace.Source.Folder.ParentFolder = workspace.Folder; } } foreach (var reference in project.References) { LoadProjects(reference.Project); } } /// public override void OnInit() { FlaxEngine.Content.AssetDisposing += OnContentAssetDisposing; // Setup content proxies Proxy.Add(new TextureProxy()); Proxy.Add(new ModelProxy()); Proxy.Add(new SkinnedModelProxy()); Proxy.Add(new MaterialProxy()); Proxy.Add(new MaterialInstanceProxy()); Proxy.Add(new MaterialFunctionProxy()); Proxy.Add(new SpriteAtlasProxy()); Proxy.Add(new CubeTextureProxy()); Proxy.Add(new PreviewsCacheProxy()); Proxy.Add(new FontProxy()); Proxy.Add(new ShaderProxy()); Proxy.Add(new ShaderSourceProxy()); Proxy.Add(new ParticleEmitterProxy()); Proxy.Add(new ParticleEmitterFunctionProxy()); Proxy.Add(new ParticleSystemProxy()); Proxy.Add(new SceneAnimationProxy()); Proxy.Add(new CSharpScriptProxy()); Proxy.Add(new CppAssetProxy()); Proxy.Add(new CppStaticClassProxy()); Proxy.Add(new CppScriptProxy()); Proxy.Add(new SceneProxy()); Proxy.Add(new PrefabProxy()); Proxy.Add(new IESProfileProxy()); Proxy.Add(new CollisionDataProxy()); Proxy.Add(new AudioClipProxy()); Proxy.Add(new AnimationGraphProxy()); Proxy.Add(new AnimationGraphFunctionProxy()); Proxy.Add(new AnimationProxy()); Proxy.Add(new SkeletonMaskProxy()); Proxy.Add(new GameplayGlobalsProxy()); Proxy.Add(new VisualScriptProxy()); Proxy.Add(new LocalizedStringTableProxy()); Proxy.Add(new FileProxy()); Proxy.Add(new SpawnableJsonAssetProxy()); // Settings Proxy.Add(new SettingsProxy(typeof(GameSettings), Editor.Instance.Icons.GameSettings128)); Proxy.Add(new SettingsProxy(typeof(TimeSettings), Editor.Instance.Icons.TimeSettings128)); Proxy.Add(new SettingsProxy(typeof(LayersAndTagsSettings), Editor.Instance.Icons.LayersTagsSettings128)); Proxy.Add(new SettingsProxy(typeof(PhysicsSettings), Editor.Instance.Icons.PhysicsSettings128)); Proxy.Add(new SettingsProxy(typeof(GraphicsSettings), Editor.Instance.Icons.GraphicsSettings128)); Proxy.Add(new SettingsProxy(typeof(NavigationSettings), Editor.Instance.Icons.NavigationSettings128)); Proxy.Add(new SettingsProxy(typeof(LocalizationSettings), Editor.Instance.Icons.LocalizationSettings128)); Proxy.Add(new SettingsProxy(typeof(AudioSettings), Editor.Instance.Icons.AudioSettings128)); Proxy.Add(new SettingsProxy(typeof(BuildSettings), Editor.Instance.Icons.BuildSettings128)); Proxy.Add(new SettingsProxy(typeof(InputSettings), Editor.Instance.Icons.InputSettings128)); Proxy.Add(new SettingsProxy(typeof(StreamingSettings), Editor.Instance.Icons.BuildSettings128)); Proxy.Add(new SettingsProxy(typeof(WindowsPlatformSettings), Editor.Instance.Icons.WindowsSettings128)); Proxy.Add(new SettingsProxy(typeof(UWPPlatformSettings), Editor.Instance.Icons.UWPSettings128)); Proxy.Add(new SettingsProxy(typeof(LinuxPlatformSettings), Editor.Instance.Icons.LinuxSettings128)); Proxy.Add(new SettingsProxy(typeof(AndroidPlatformSettings), Editor.Instance.Icons.AndroidSettings128)); var typePS4PlatformSettings = TypeUtils.GetManagedType(GameSettings.PS4PlatformSettingsTypename); if (typePS4PlatformSettings != null) Proxy.Add(new SettingsProxy(typePS4PlatformSettings, Editor.Instance.Icons.PlaystationSettings128)); var typeXboxOnePlatformSettings = TypeUtils.GetManagedType(GameSettings.XboxOnePlatformSettingsTypename); if (typeXboxOnePlatformSettings != null) Proxy.Add(new SettingsProxy(typeXboxOnePlatformSettings, Editor.Instance.Icons.XBOXSettings128)); var typeXboxScarlettPlatformSettings = TypeUtils.GetManagedType(GameSettings.XboxScarlettPlatformSettingsTypename); if (typeXboxScarlettPlatformSettings != null) Proxy.Add(new SettingsProxy(typeXboxScarlettPlatformSettings, Editor.Instance.Icons.XBOXSettings128)); var typeSwitchPlatformSettings = TypeUtils.GetManagedType(GameSettings.SwitchPlatformSettingsTypename); if (typeSwitchPlatformSettings != null) Proxy.Add(new SettingsProxy(typeSwitchPlatformSettings, Editor.Instance.Icons.SwitchSettings128)); // Last add generic json (won't override other json proxies) Proxy.Add(new GenericJsonAssetProxy()); // Create content folders nodes var startTime = Platform.TimeSeconds; Engine = new ProjectTreeNode(Editor.EngineProject) { Content = new MainContentTreeNode(Engine, ContentFolderType.Content, Globals.EngineContentFolder), }; if (Editor.GameProject != Editor.EngineProject) { Game = new ProjectTreeNode(Editor.GameProject) { Content = new MainContentTreeNode(Game, ContentFolderType.Content, Globals.ProjectContentFolder), Source = new MainContentTreeNode(Game, ContentFolderType.Source, Globals.ProjectSourceFolder), }; // TODO: why it's required? the code above should work for linking the nodes hierarchy Game.Content.Folder.ParentFolder = Game.Folder; Game.Source.Folder.ParentFolder = Game.Folder; Projects.Add(Game); } Engine.Content.Folder.ParentFolder = Engine.Folder; Projects.Add(Engine); if (Editor.GameProject != Editor.EngineProject) { LoadProjects(Game.Project); } // Load all folders // TODO: we should create async task for gathering content and whole workspace contents if it takes too long // TODO: create progress bar in content window and after end we should enable events and update it _isDuringFastSetup = true; foreach (var project in Projects) { if (project.Content != null) LoadFolder(project.Content, true); if (project.Source != null) LoadFolder(project.Source, true); } _isDuringFastSetup = false; // Enable events _enableEvents = true; Editor.ContentImporting.ImportFileEnd += ContentImporting_ImportFileDone; var endTime = Platform.TimeSeconds; Editor.Log(string.Format("Project database created in {0} ms. Items count: {1}", (int)((endTime - startTime) * 1000.0), _itemsCreated)); } private void ContentImporting_ImportFileDone(IFileEntryAction obj, bool failed) { if (failed) return; // Check if already has that element var item = Find(obj.ResultUrl); if (item is BinaryAssetItem binaryAssetItem) { // Get asset info from the registry (content layer will update cache it just after import) if (FlaxEngine.Content.GetAssetInfo(binaryAssetItem.Path, out var assetInfo)) { // If asset type id has been changed we HAVE TO close all windows that use it // For eg. change texture to sprite atlas on reimport if (binaryAssetItem.TypeName != assetInfo.TypeName) { // Asset type has been changed! Editor.LogWarning(string.Format("Asset \'{0}\' changed type from {1} to {2}", item.Path, binaryAssetItem.TypeName, assetInfo.TypeName)); Editor.Windows.CloseAllEditors(item); // Remove this item from the database and some related data var toRefresh = binaryAssetItem.ParentFolder; binaryAssetItem.Dispose(); toRefresh.Children.Remove(binaryAssetItem); if (!binaryAssetItem.HasDefaultThumbnail) { // Delete old thumbnail and remove it from the cache Editor.Instance.Thumbnails.DeletePreview(binaryAssetItem); } // Refresh the parent folder to find the new asset (it should have different type or some other format) RefreshFolder(toRefresh, false); } else { // Refresh element data that could change during importing binaryAssetItem.OnReimport(ref assetInfo.ID); } } // Refresh content view (not the best design because window could also track this event but it gives better performance) Editor.Windows.ContentWin?.RefreshView(); } } internal void OnDirectoryEvent(MainContentTreeNode node, FileSystemEventArgs e) { // Ensure to be ready for external events if (_isDuringFastSetup) return; // TODO: maybe we could make it faster! since we have a path so it would be easy to just create or delete given file. but remember about subdirectories // Switch type switch (e.ChangeType) { case WatcherChangeTypes.Created: case WatcherChangeTypes.Deleted: { lock (_dirtyNodes) { _dirtyNodes.Add(node); } break; } } } /// public override void OnUpdate() { // Update all dirty content tree nodes lock (_dirtyNodes) { foreach (var node in _dirtyNodes) { LoadFolder(node, true); if (_enableEvents) WorkspaceModified?.Invoke(); } _dirtyNodes.Clear(); } } /// public override void OnExit() { FlaxEngine.Content.AssetDisposing -= OnContentAssetDisposing; // Disable events _enableEvents = false; // Cleanup Proxy.ForEach(x => x.Dispose()); if (Game != null) { Game.Dispose(); Game = null; } if (Engine != null) { Engine.Dispose(); Engine = null; } Proxy.Clear(); } } }