Files
FlaxEngine/Source/Editor/Modules/ContentImportingModule.cs
2022-01-14 13:31:12 +01:00

450 lines
17 KiB
C#

// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using FlaxEditor.Content;
using FlaxEditor.Content.Create;
using FlaxEditor.Content.Import;
using FlaxEngine;
namespace FlaxEditor.Modules
{
/// <summary>
/// Imports assets and other resources to the project. Provides per asset import settings editing.
/// </summary>
/// <seealso cref="FlaxEditor.Modules.EditorModule" />
public sealed class ContentImportingModule : EditorModule
{
// Amount of requests done/total used to calculate importing progress
private int _importBatchDone;
private int _importBatchSize;
// Firstly service is collecting import requests and then performs actual importing in the background.
private readonly Queue<IFileEntryAction> _importingQueue = new Queue<IFileEntryAction>();
private readonly List<Request> _requests = new List<Request>();
private long _workerEndFlag;
private Thread _workerThread;
/// <summary>
/// Gets a value indicating whether this instance is importing assets.
/// </summary>
public bool IsImporting => _importBatchSize > 0;
/// <summary>
/// Gets the importing assets progress.
/// </summary>
public float ImportingProgress => _importBatchSize > 0 ? (float)_importBatchDone / _importBatchSize : 1.0f;
/// <summary>
/// Gets the amount of files done in the current import batch.
/// </summary>
public float ImportBatchDone => _importBatchDone;
/// <summary>
/// Gets the size of the current import batch (imported files + files to import left).
/// </summary>
public int ImportBatchSize => _importBatchSize;
/// <summary>
/// Occurs when assets importing starts.
/// </summary>
public event Action ImportingQueueBegin;
/// <summary>
/// Occurs when file is being imported.
/// </summary>
public event Action<IFileEntryAction> ImportFileBegin;
/// <summary>
/// Import file end delegate.
/// </summary>
/// <param name="entry">The imported file entry.</param>
/// <param name="failed">if set to <c>true</c> if importing failed, otherwise false.</param>
public delegate void ImportFileEndDelegate(IFileEntryAction entry, bool failed);
/// <summary>
/// Occurs when file importing end.
/// </summary>
public event ImportFileEndDelegate ImportFileEnd;
/// <summary>
/// Occurs when assets importing ends.
/// </summary>
public event Action ImportingQueueEnd;
/// <inheritdoc />
internal ContentImportingModule(Editor editor)
: base(editor)
{
}
/// <summary>
/// Creates the specified file entry (can show create settings dialog if needed).
/// </summary>
/// <param name="entry">The entry.</param>
public void Create(CreateFileEntry entry)
{
if (entry.HasSettings)
{
// Use settings dialog
var dialog = new CreateFilesDialog(entry);
dialog.Show(Editor.Windows.MainWindow);
}
else
{
// Use direct creation
LetThemBeCreatedxD(entry);
}
}
/// <summary>
/// Shows the dialog for selecting files to import.
/// </summary>
/// <param name="targetLocation">The target location.</param>
public void ShowImportFileDialog(ContentFolder targetLocation)
{
// Ask user to select files to import
if (FileSystem.ShowOpenFileDialog(Editor.Windows.MainWindow, null, "All files (*.*)\0*.*\0", true, "Select files to import", out var files))
return;
if (files != null && files.Length > 0)
{
Import(files, targetLocation);
}
}
/// <summary>
/// Reimports the specified <see cref="BinaryAssetItem"/> item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="settings">The import settings to override.</param>
/// <param name="skipSettingsDialog">True if skip any popup dialogs showing for import options adjusting. Can be used when importing files from code.</param>
public void Reimport(BinaryAssetItem item, object settings = null, bool skipSettingsDialog = false)
{
if (item != null && !item.GetImportPath(out string importPath))
{
// Check if input file is missing
if (!System.IO.File.Exists(importPath))
{
Editor.LogWarning(string.Format("Cannot reimport asset \'{0}\'. File \'{1}\' does not exist.", item.Path, importPath));
if (skipSettingsDialog)
return;
// Ask user to select new file location
var title = string.Format("Please find missing \'{0}\' file for asset \'{1}\'", importPath, item.ShortName);
if (FileSystem.ShowOpenFileDialog(Editor.Windows.MainWindow, null, "All files (*.*)\0*.*\0", false, title, out var files))
return;
if (files != null && files.Length > 0)
importPath = files[0];
// Validate file path again
if (!System.IO.File.Exists(importPath))
return;
}
Import(importPath, item.Path, true, skipSettingsDialog, settings);
}
}
/// <summary>
/// Imports the specified files.
/// </summary>
/// <param name="files">The files.</param>
/// <param name="targetLocation">The target location.</param>
/// <param name="skipSettingsDialog">True if skip any popup dialogs showing for import options adjusting. Can be used when importing files from code.</param>
public void Import(IEnumerable<string> files, ContentFolder targetLocation, bool skipSettingsDialog = false)
{
if (targetLocation == null)
throw new ArgumentNullException();
if (files == null)
return;
lock (_requests)
{
bool skipDialog = skipSettingsDialog;
foreach (var file in files)
{
Import(file, targetLocation, skipSettingsDialog, null, ref skipDialog);
}
}
}
/// <summary>
/// Imports the specified file.
/// </summary>
/// <param name="file">The file.</param>
/// <param name="targetLocation">The target location.</param>
/// <param name="skipSettingsDialog">True if skip any popup dialogs showing for import options adjusting. Can be used when importing files from code.</param>
/// <param name="settings">Import settings to override. Use null to skip this value.</param>
public void Import(string file, ContentFolder targetLocation, bool skipSettingsDialog = false, object settings = null)
{
bool skipDialog = skipSettingsDialog;
Import(file, targetLocation, skipSettingsDialog, settings, ref skipDialog);
}
private void Import(string inputPath, ContentFolder targetLocation, bool skipSettingsDialog, object settings, ref bool skipDialog)
{
if (targetLocation == null)
throw new ArgumentNullException();
var extension = System.IO.Path.GetExtension(inputPath) ?? string.Empty;
// Check if given file extension is a binary asset (.flax files) and can be imported by the engine
bool isBuilt = Editor.CanImport(extension, out var outputExtension);
if (isBuilt)
{
outputExtension = '.' + outputExtension;
if (!targetLocation.CanHaveAssets)
{
// Error
Editor.LogWarning(string.Format("Cannot import \'{0}\' to \'{1}\'. The target directory cannot have assets.", inputPath, targetLocation.Node.Path));
if (!skipDialog)
{
skipDialog = true;
MessageBox.Show("Target location cannot have assets. Use Content folder for your game assets.", "Cannot import assets", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
return;
}
}
else
{
// Preserve file extension (will copy file to the import location)
outputExtension = extension;
// Check if can place source files here
if (!targetLocation.CanHaveScripts && (extension == ".cs" || extension == ".cpp" || extension == ".h"))
{
// Error
Editor.LogWarning(string.Format("Cannot import \'{0}\' to \'{1}\'. The target directory cannot have scripts.", inputPath, targetLocation.Node.Path));
if (!skipDialog)
{
skipDialog = true;
MessageBox.Show("Target location cannot have scripts. Use Source folder for your game source code.", "Cannot import assets", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
return;
}
}
var shortName = System.IO.Path.GetFileNameWithoutExtension(inputPath);
var outputPath = System.IO.Path.Combine(targetLocation.Path, shortName + outputExtension);
Import(inputPath, outputPath, isBuilt, skipSettingsDialog, settings);
}
/// <summary>
/// Imports the specified file to the target destination.
/// Actual importing is done later after gathering settings from the user via <see cref="ImportFilesDialog"/>.
/// </summary>
/// <param name="inputPath">The input path.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="isInBuilt">True if use in-built importer (engine backend).</param>
/// <param name="skipSettingsDialog">True if skip any popup dialogs showing for import options adjusting. Can be used when importing files from code.</param>
/// <param name="settings">Import settings to override. Use null to skip this value.</param>
private void Import(string inputPath, string outputPath, bool isInBuilt, bool skipSettingsDialog = false, object settings = null)
{
lock (_requests)
{
_requests.Add(new Request
{
InputPath = inputPath,
OutputPath = outputPath,
IsInBuilt = isInBuilt,
SkipSettingsDialog = skipSettingsDialog,
Settings = settings,
});
}
}
private void WorkerMain()
{
IFileEntryAction entry;
bool wasLastTickWorking = false;
while (Interlocked.Read(ref _workerEndFlag) == 0)
{
// Try to get entry to process
lock (_requests)
{
if (_importingQueue.Count > 0)
entry = _importingQueue.Dequeue();
else
entry = null;
}
// Check if has any no job
bool inThisTickWork = entry != null;
if (inThisTickWork)
{
// Check if begin importing
if (!wasLastTickWorking)
{
_importBatchDone = 0;
ImportingQueueBegin?.Invoke();
}
// Import file
bool failed = true;
try
{
ImportFileBegin?.Invoke(entry);
failed = entry.Execute();
}
catch (Exception ex)
{
Editor.LogWarning(ex);
}
finally
{
if (failed)
{
Editor.LogWarning("Failed to import " + entry.SourceUrl + " to " + entry.ResultUrl);
}
_importBatchDone++;
ImportFileEnd?.Invoke(entry, failed);
}
}
else
{
// Check if end importing
if (wasLastTickWorking)
{
_importBatchDone = _importBatchSize = 0;
ImportingQueueEnd?.Invoke();
}
// Wait some time
Thread.Sleep(100);
}
wasLastTickWorking = inThisTickWork;
}
}
internal void LetThemBeImportedxD(List<ImportFileEntry> entries)
{
int count = entries.Count;
if (count > 0)
{
lock (_requests)
{
_importBatchSize += count;
for (int i = 0; i < count; i++)
_importingQueue.Enqueue(entries[i]);
}
StartWorker();
}
}
internal void LetThemBeCreatedxD(CreateFileEntry entry)
{
lock (_requests)
{
_importBatchSize += 1;
_importingQueue.Enqueue(entry);
}
StartWorker();
}
private void StartWorker()
{
if (_workerThread != null)
return;
_workerEndFlag = 0;
_workerThread = new Thread(WorkerMain)
{
Name = "Content Importer",
Priority = ThreadPriority.Highest
};
_workerThread.Start();
}
private void EndWorker()
{
if (_workerThread == null)
return;
Interlocked.Increment(ref _workerEndFlag);
Thread.Sleep(0);
_workerThread.Join(1000);
_workerThread.Abort();
_workerThread = null;
}
/// <inheritdoc />
public override void OnInit()
{
ImportFileEntry.RegisterDefaultTypes();
}
/// <inheritdoc />
public override void OnUpdate()
{
// Check if has no requests to process
if (_requests.Count == 0)
return;
lock (_requests)
{
try
{
// Get entries
List<ImportFileEntry> entries = new List<ImportFileEntry>(_requests.Count);
bool needSettingsDialog = false;
for (int i = 0; i < _requests.Count; i++)
{
var request = _requests[i];
var entry = ImportFileEntry.CreateEntry(ref request);
if (entry != null)
{
if (request.Settings != null && entry.TryOverrideSettings(request.Settings))
{
// Use overridden settings
}
else if (!request.SkipSettingsDialog)
{
needSettingsDialog |= entry.HasSettings;
}
entries.Add(entry);
}
}
_requests.Clear();
// Check if need to show importing dialog or can just pass requests
if (needSettingsDialog)
{
var dialog = new ImportFilesDialog(entries);
dialog.Show(Editor.Windows.MainWindow);
dialog.Focus();
}
else
{
LetThemBeImportedxD(entries);
}
}
catch (Exception ex)
{
// Error
Editor.LogWarning(ex);
Editor.LogError("Failed to process files import request.");
}
}
}
/// <inheritdoc />
public override void OnExit()
{
EndWorker();
}
}
}