Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
AssetImportSession.cs
Go to the documentation of this file.
1 // Copyright (c) 2014 Silicon Studio Corp. (http://siliconstudio.co.jp)
2 // This file is distributed under GPL v3. See LICENSE.md for details.
3 using System;
4 using System.Collections.Generic;
5 using System.Diagnostics;
6 using System.IO;
7 using System.Linq;
8 using System.Threading;
9 using SiliconStudio.Assets.Analysis;
10 using SiliconStudio.Assets.Diff;
11 using SiliconStudio.Assets.Visitors;
12 using SiliconStudio.Core;
13 using SiliconStudio.Core.Diagnostics;
14 using SiliconStudio.Core.Extensions;
15 using SiliconStudio.Core.IO;
16 using SiliconStudio.Core.Serialization;
17 
18 namespace SiliconStudio.Assets
19 {
20  /// <summary>
21  /// This class is handling importItem of assets into a session. See remarks for usage.
22  /// </summary>
23  /// <remarks>
24  /// <code>
25  /// var importSession = new AssetImportSession(session);
26  /// // First add files to the session
27  /// importSession.AddFile("C:\xxx\yyy\test.fbx", package, "zzz");
28  ///
29  /// // accessing importSession.Imports will return the list of files to importItem
30  ///
31  /// // Prepare files for importItem.
32  /// importSession.Stage();
33  ///
34  /// // importSession.Imports.Items contains the list of items that will be imported
35  ///
36  /// // Here we need to select the assets that will be used for merge if there are any
37  /// foreach(var fileItem in importSession.Imports)
38  /// {
39  /// foreach(var importItemByImporter in importSession.ByImporters)
40  /// {
41  /// foreach(var importItem in importItemByImporter.Items)
42  /// {
43  /// // Select for example the first mergeable item
44  /// importItem.SelectedItem = (importItem.Merges.Count > 0) ? importItem.Merges[0].PreviousItem : importItem.Item;
45  /// }
46  /// }
47  /// }
48  ///
49  /// // Merge assets if necessary
50  /// importSession.Merge();
51  ///
52  /// // Import all assets
53  /// importSession.Import();
54  /// </code>
55  /// </remarks>
56  public sealed class AssetImportSession
57  {
58  private readonly PackageSession session;
59  private readonly List<AssetToImport> imports;
60 
61  // Associate source assets (fbx...etc.) to asset being imported
62  private readonly Dictionary<AssetLocationTyped, HashSet<AssetItem>> sourceFileToAssets = new Dictionary<AssetLocationTyped, HashSet<AssetItem>>();
63 
64  /// <summary>
65  /// Occurs when this import session is making progress.
66  /// </summary>
67  public event EventHandler<AssetImportSessionEvent> Progress;
68 
69  /// <summary>
70  /// Initializes a new instance of the <see cref="AssetImportSession"/> class.
71  /// </summary>
72  /// <param name="session">The session.</param>
73  /// <exception cref="System.ArgumentNullException">session</exception>
75  {
76  if (session == null) throw new ArgumentNullException("session");
77  this.session = session;
78  imports = new List<AssetToImport>();
79  }
80 
81  /// <summary>
82  /// Gets the list of import being processed by this instance.
83  /// </summary>
84  /// <value>The imports.</value>
85  public List<AssetToImport> Imports
86  {
87  get
88  {
89  return imports;
90  }
91  }
92 
93  /// <summary>
94  /// Gets a value indicating whether this instance has errors.
95  /// </summary>
96  /// <value><c>true</c> if this instance has errors; otherwise, <c>false</c>.</value>
97  public bool HasErrors
98  {
99  get
100  {
101  return Imports.SelectMany(import => import.ByImporters).Any(step => step.HasErrors);
102  }
103  }
104 
105  /// <summary>
106  /// Determines whether the specified file is supported
107  /// </summary>
108  /// <param name="file">The file.</param>
109  /// <returns><c>true</c> if the specified file is supported; otherwise, <c>false</c>.</returns>
110  /// <exception cref="System.ArgumentNullException">file</exception>
111  public bool IsFileSupported(UFile file)
112  {
113  if (file == null) throw new ArgumentNullException("file");
114  if (!file.IsAbsolute) return false;
115  if (file.GetFileExtension() == null) return false;
116  if (!File.Exists(file)) return false;
117 
118  return true;
119  }
120 
121  /// <summary>
122  /// Adds a file to import.
123  /// </summary>
124  /// <param name="file">The file.</param>
125  /// <param name="package">The package where to import this file.</param>
126  /// <param name="directory">The directory relative to package where to import this file.</param>
127  /// <exception cref="System.ArgumentNullException">
128  /// file
129  /// or
130  /// package
131  /// or
132  /// directory
133  /// </exception>
134  /// <exception cref="System.ArgumentException">File [{0}] does not exist or is not an absolute path.ToFormat(file)</exception>
135  public AssetToImport AddFile(UFile file, Package package, UDirectory directory)
136  {
137  if (file == null) throw new ArgumentNullException("file");
138  if (package == null) throw new ArgumentNullException("package");
139  if (directory == null) throw new ArgumentNullException("directory");
140  if (!IsFileSupported(file))
141  {
142  throw new ArgumentException("File [{0}] does not exist or is not an absolute path".ToFormat(file));
143  }
144 
145  // Sort by importer display rank
146  var importerList = AssetRegistry.FindImporterByExtension(file.GetFileExtension()).ToList();
147  importerList.Sort((left, right) => -left.DisplayRank.CompareTo(right.DisplayRank));
148 
149  AssetToImport assetToImport = null;
150  foreach (var importer in importerList)
151  {
152  assetToImport = AddFile(file, importer, package, directory);
153  }
154  return assetToImport;
155  }
156 
157  /// <summary>
158  /// Adds files to import.
159  /// </summary>
160  /// <param name="files">The files.</param>
161  /// <param name="package">The package where to import this file.</param>
162  /// <param name="directory">The directory relative to package where to import this file.</param>
163  /// <exception cref="System.ArgumentNullException">
164  /// files
165  /// or
166  /// package
167  /// or
168  /// directory
169  /// </exception>
171  {
172  if (files == null) throw new ArgumentNullException("files");
173  if (package == null) throw new ArgumentNullException("package");
174  if (directory == null) throw new ArgumentNullException("directory");
175  var result = new List<AssetToImport>();
176  foreach (var file in files)
177  {
178  var assetToImport = AddFile(file, package, directory);
179  if (assetToImport != null && !result.Contains(assetToImport))
180  result.Add(assetToImport);
181  }
182  return result;
183  }
184 
185  /// <summary>
186  /// Adds a file to import.
187  /// </summary>
188  /// <param name="file">The file.</param>
189  /// <param name="importer">The associated importer to this file.</param>
190  /// <param name="package">The package where to import this file.</param>
191  /// <param name="directory">The directory relative to package where to import this file.</param>
192  /// <exception cref="System.ArgumentNullException">
193  /// file
194  /// or
195  /// importer
196  /// or
197  /// package
198  /// or
199  /// directory
200  /// or
201  /// importer [{0}] is not supporting file [{1}].ToFormat(importer.Name, file)
202  /// </exception>
203  /// <exception cref="System.ArgumentException">File [{0}] does not exist or is not an absolute path.ToFormat(file)</exception>
204  /// <exception cref="System.InvalidOperationException">Current session does not contain package</exception>
205  public AssetToImport AddFile(UFile file, IAssetImporter importer, Package package, UDirectory directory)
206  {
207  if (file == null) throw new ArgumentNullException("file");
208  if (importer == null) throw new ArgumentNullException("importer");
209  if (package == null) throw new ArgumentNullException("package");
210  if (directory == null) throw new ArgumentNullException("directory");
211 
212  if (!IsFileSupported(file))
213  {
214  throw new ArgumentException("File [{0}] does not exist or is not an absolute path".ToFormat(file));
215  }
216 
217  if (!importer.IsSupportingFile(file)) throw new ArgumentNullException("importer [{0}] is not supporting file [{1}]".ToFormat(importer.Name, file));
218 
219  if (!session.Packages.Contains(package))
220  {
221  throw new InvalidOperationException("Current session does not contain package");
222  }
223 
224  return RegisterImporter(file, package, directory, importer);
225  }
226 
227  /// <summary>
228  /// Determines whether the specified asset is supporting re-import.
229  /// </summary>
230  /// <param name="assetItem">The asset item.</param>
231  /// <returns><c>true</c> if the specified asset is supporting re-import; otherwise, <c>false</c>.</returns>
232  /// <exception cref="System.ArgumentNullException">assetItem</exception>
234  {
235  if (assetItem == null) throw new ArgumentNullException("assetItem");
236  if (assetItem.Package == null) return false;
237  if (assetItem.Package.Session != session) return false;
238 
239  var asset = assetItem.Asset as AssetImportTracked;
240  if (asset == null) return false;
241  if (asset.Base == null) return false;
242  if (asset.Source == null) return false;
243 
244  var baseAsset = asset.Base.Asset as AssetImportTracked;
245  if (baseAsset == null) return false;
246 
247  if (baseAsset.ImporterId.HasValue)
248  {
249  var importer = AssetRegistry.FindImporterById(baseAsset.ImporterId.Value);
250  if (importer == null)
251  return false;
252  }
253 
254  return true;
255  }
256 
257  /// <summary>
258  /// Adds an existing asset for reimport
259  /// </summary>
260  /// <param name="assetItem">The asset item.</param>
261  /// <exception cref="System.ArgumentNullException">assetItem</exception>
263  {
264  if (assetItem == null) throw new ArgumentNullException("assetItem");
265  if (assetItem.Package == null) throw new ArgumentException("AssetItem is not attached to a package");
266  if (assetItem.Package.Session != session) throw new ArgumentException("AssetItem is not attached to the same session of this importItem");
267 
268  var asset = assetItem.Asset as AssetImportTracked;
269  if (asset == null) throw new ArgumentException("The asset is not an existing importable asset");
270  if (asset.Base == null) throw new ArgumentException("The asset to importItem must have a base to reimport");
271  if (asset.Source == null) throw new ArgumentException("The asset to importItem has no source/location to an existing raw asset");
272 
273  var baseAsset = asset.Base.Asset as AssetImportTracked;
274  if (baseAsset == null) throw new ArgumentException("The base asset to importItem is invalid");
275 
276  IAssetImporter importer = null;
277  // Try to use the previous importer if it exists
278  if (baseAsset.ImporterId.HasValue)
279  {
280  importer = AssetRegistry.FindImporterById(baseAsset.ImporterId.Value);
281  }
282 
283  // If not, take the first default importer
284  if (importer == null)
285  {
286  importer = AssetRegistry.FindImporterByExtension(asset.Source.GetFileExtension()).FirstOrDefault();
287  }
288 
289  if (importer == null)
290  {
291  throw new ArgumentException("No importer found for this asset item");
292  }
293 
294  return RegisterImporter(asset.Source, assetItem.Package, assetItem.Location.GetDirectory(), importer, assetItem);
295  }
296 
297  /// <summary>
298  /// Analyze files for preparing them for the merge and import steps. This must be called first
299  /// after calling <see cref="AddFile(SiliconStudio.Core.IO.UFile,Package,SiliconStudio.Core.IO.UDirectory)"/> methods.
300  /// </summary>
301  /// <returns><c>true</c> if staging was successfull otherwise. See remarks for checking errors</returns>
302  /// <remarks>
303  /// If this method returns false, errors should be checked on each <see cref="AssetToImport"/> from the <see cref="Imports"/> list.
304  /// </remarks>
305  public bool Stage(CancellationToken? cancelToken = null)
306  {
307  bool isImportOk = true;
308  var ids = new HashSet<Guid>();
309 
310  // Clear previously created items
311  foreach (var fileToImport in Imports)
312  {
313  foreach (var toImportByImporter in fileToImport.ByImporters)
314  {
315  toImportByImporter.Items.Clear();
316  toImportByImporter.Log.Clear();
317  }
318  }
319 
320  // Process files to import
321  foreach (var fileToImport in Imports)
322  {
323  if (!fileToImport.Enabled)
324  {
325  continue;
326  }
327 
328  foreach (var toImportByImporter in fileToImport.ByImporters)
329  {
330  // Skip importers that are not activated or for which output types are not selected
331  if (!toImportByImporter.Enabled || !toImportByImporter.ImporterParameters.HasSelectedOutputTypes)
332  {
333  continue;
334  }
335 
336  if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
337  {
338  toImportByImporter.Log.Warning("Cancellation requested before importing asset [{0}] with importer [{1}]", fileToImport.File, toImportByImporter.Importer.Name);
339  return false;
340  }
341 
342  OnProgress(AssetImportSessionEventType.Begin, AssetImportSessionStepType.Staging, toImportByImporter);
343  try
344  {
345  // Call the importer. For some assts, this operation can take some time
346  var itemsToImport = toImportByImporter.Importer.Import(fileToImport.File, toImportByImporter.ImporterParameters).ToList();
347  CheckAssetsToImport(itemsToImport, ids);
348 
349  foreach (var itemToImport in itemsToImport)
350  {
351  if (itemToImport == null)
352  continue;
353 
354  itemToImport.SourceFolder = fileToImport.Package.GetDefaultAssetFolder();
355 
356  var assetToImport = new AssetToImportMergeGroup(toImportByImporter, itemToImport);
357  toImportByImporter.Items.Add(assetToImport);
358  }
359 
360  toImportByImporter.Log.Info("Successfully processed file [{0}]", fileToImport.File);
361  }
362  catch (Exception ex)
363  {
364  toImportByImporter.Log.Error("Unexpected exception while importing file [{0}]", ex, fileToImport.File);
365  isImportOk = false;
366  }
367  finally
368  {
369  OnProgress(AssetImportSessionEventType.End, AssetImportSessionStepType.Staging, toImportByImporter);
370  }
371  }
372  }
373 
374  // Compute hash for new assets
375  ComputeAssetHash(cancelToken);
376 
377  // Prepare assets for merge if any
378  PrepareMerge(cancelToken);
379 
380  return isImportOk;
381  }
382 
383  /// <summary>
384  /// Merges each asset with the selected asset specified in <see cref="AssetToImportMergeGroup.SelectedItem"/>
385  /// </summary>
386  /// <param name="cancelToken">The cancel token.</param>
387  public void Merge(CancellationToken? cancelToken = null)
388  {
389  var idRemapping = new Dictionary<Guid, AssetItem>();
390 
391  // Generates a list of remapping, between the original asset that we are trying to import
392  // and the selected asset we are going to merge into.
393  foreach (var fileToImport in Imports.Where(it => it.Enabled))
394  {
395  foreach (var toImportByImporter in fileToImport.ByImporters.Where(it => it.Enabled))
396  {
397  foreach (var toImport in toImportByImporter.Items)
398  {
399  if (toImport.SelectedItem != null)
400  {
401  if (toImport.Item.Id != toImport.SelectedItem.Id)
402  {
403  idRemapping.Add(toImport.Item.Id, toImport.SelectedItem);
404  }
405  }
406  else if (toImport.Enabled)
407  {
408  // By default set selected item from new if nothing selected
409  toImport.SelectedItem = toImport.Item;
410  }
411  }
412  }
413  }
414 
415  if (idRemapping.Count == 0)
416  {
417  return;
418  }
419 
420  // Perform the merge for each asset
421  foreach (var fileToImport in Imports.Where(it => it.Enabled))
422  {
423  foreach (var toImportByImporter in fileToImport.ByImporters.Where(it => it.Enabled))
424  {
425  foreach (var toImport in toImportByImporter.Items.Where(it => it.Enabled))
426  {
427  // If the asset is not being merged, we still need to fix references to the real asset that will be
428  // imported into the package
429  if (toImport.SelectedItem == null || toImport.Item.Id == toImport.SelectedItem.Id)
430  {
431  FixAssetReferences(toImport, idRemapping);
432  continue;
433  }
434 
435  var selectedItem = toImport.SelectedItem;
436  var selectedMerge = toImport.Merges.FirstOrDefault(merge => merge.PreviousItem == selectedItem);
437 
438  if (selectedMerge == null)
439  {
440  toImport.Log.Error("Selected item [{0}] does not exist in the merge");
441  continue;
442  }
443 
444  if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
445  {
446  toImport.Log.Warning("Cancellation requested before merging asset from [{0}] to location [{1}] ", fileToImport.File, selectedItem.Location);
447  return;
448  }
449 
450  OnProgress(AssetImportSessionEventType.Begin, AssetImportSessionStepType.Merging, toImport);
451  try
452  {
453  MergeAsset(toImport, selectedMerge, idRemapping);
454  }
455  catch (Exception ex)
456  {
457  toImport.Log.Error("Unexpected error while merging asset [{0}]", ex, toImport.Item);
458  }
459  finally
460  {
461  OnProgress(AssetImportSessionEventType.End, AssetImportSessionStepType.Merging, toImport);
462  }
463  }
464  }
465  }
466  }
467 
468  /// <summary>
469  /// Fixes asset references for asset being imported but not being merged. See remarks.
470  /// </summary>
471  /// <param name="toImport">To import.</param>
472  /// <param name="idRemapping">The identifier remapping.</param>
473  /// <remarks>
474  /// As it is possible to re-use existing asset when importing but also at the same time to create new asset,
475  /// new need to update references from asset not being merged to assets that are merged back to an existing asset in
476  /// the package.
477  /// </remarks>
478  private static void FixAssetReferences(AssetToImportMergeGroup toImport, Dictionary<Guid, AssetItem> idRemapping)
479  {
480  var asset = (toImport.SelectedItem ?? toImport.Item).Asset;
481 
482  // Fix assets references
483  var referencesToUpdate = AssetReferenceAnalysis.Visit(asset);
484  foreach (var assetReferenceLink in referencesToUpdate)
485  {
486  var refToUpdate = assetReferenceLink.Reference as IContentReference;
487  if (refToUpdate == null || refToUpdate.Id == Guid.Empty || !idRemapping.ContainsKey(refToUpdate.Id))
488  {
489  continue;
490  }
491 
492  var realItem = idRemapping[refToUpdate.Id];
493  assetReferenceLink.UpdateReference(realItem.Id, realItem.Location);
494  }
495  }
496 
497  private static void MergeAsset(AssetToImportMergeGroup toImport, AssetToImportMerge selectedMerge, Dictionary<Guid, AssetItem> idRemapping)
498  {
499  // Perform a full merge which will replace output references if necessary
500  var result = AssetMerge.Merge(selectedMerge.Diff, node =>
501  {
502  // Asset references are a special case while importing, as we are
503  // going to try to rematch them, so if they changed, we expect to
504  // use the original instance
505  if (typeof(IContentReference).IsAssignableFrom(node.InstanceType))
506  {
507  if (node.Asset2Node != null)
508  {
509  var instance = (IContentReference)node.Asset2Node.Instance;
510  AssetItem realItem;
511  // Ids are remapped, so we are going to remap here, both on the
512  // new base and on the merged result
513  if (idRemapping.TryGetValue(instance.Id, out realItem))
514  {
515  var newReference = AssetReference.New(instance.GetType(), realItem.Id, realItem.Location);
516  node.ReplaceValue(newReference, diff3Node => diff3Node.Asset2Node, false);
517  }
518 
519  return Diff3ChangeType.MergeFromAsset2;
520  }
521 
522  if (node.Asset1Node != null)
523  {
524  return Diff3ChangeType.MergeFromAsset1;
525  }
526  }
527  return AssetMergePolicies.MergePolicyAsset2AsNewBaseOfAsset1(node);
528  });
529 
530  toImport.MergedResult = result;
531  if (result.HasErrors)
532  {
533  toImport.Log.Error("Error while trying to merge asset [{0}]", toImport.Item);
534  result.CopyTo(toImport.Log);
535  return;
536  }
537 
538  var finalAsset = result.Asset;
539  finalAsset.Base = new AssetBase(selectedMerge.Diff.Asset2);
540 
541  // Set the final item
542  toImport.MergedItem = new AssetItem(toImport.SelectedItem.Location, finalAsset) { SourceFolder = toImport.SelectedItem.SourceFolder };
543  }
544 
545 
546  /// <summary>
547  /// Imports all assets
548  /// </summary>
549  /// <returns>Result of the import.</returns>
551  {
552  var result = new ImportResult();
553  Import(result);
554  return result;
555  }
556 
557  /// <summary>
558  /// Imports all assets
559  /// </summary>
560  /// <returns>Results of the import where logs will be outputed.</returns>
561  public void Import(ImportResult result)
562  {
563  // Clears the result before appending to it
564  result.Clear();
565 
566  var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
567 
568  // Gets the list of assets merged that will be imported
569  var mergedAssets = new HashSet<Guid>();
570  foreach (var toImport in AllImports())
571  {
572  if (toImport.MergedItem != null)
573  {
574  mergedAssets.Add(toImport.MergedItem.Id);
575 
576  // If there are any errors on the selected merged result, output them in the final log
577  if (toImport.MergedResult.HasErrors)
578  {
579  result.Error("Cannot select a merge asset [{0}] that has the following merge errors:", toImport.MergedItem);
580  toImport.MergedResult.CopyTo(result);
581  }
582  }
583  }
584 
585  // Before importing, we have to fix names
586  foreach (var fileToImport in Imports.Where(it => it.Enabled))
587  {
588  var assetPackage = fileToImport.Package;
589  var assetResolver = AssetResolver.FromPackage(assetPackage);
590 
591  foreach (var toImportByImporter in fileToImport.ByImporters.Where(it => it.Enabled))
592  {
593  // Copy errors from intermediate log to output
594  toImportByImporter.Log.CopyTo(result);
595 
596  // If it has errors, don't try to importItem it
597  if (toImportByImporter.HasErrors)
598  {
599  result.Warning("Unexpected errors while importing source [{0}] with importer [{1}]. Check the details errors log", fileToImport.File, toImportByImporter.Importer.Name);
600  }
601 
602  foreach (var toImport in toImportByImporter.Items.Where(it => it.Enabled))
603  {
604  // Copy errors from intermediate log to output
605  toImport.Log.CopyTo(result);
606 
607  // If the item is in error, don't try to import it
608  if (toImport.HasErrors)
609  {
610  result.Warning("Unexpected errors while importing asset [{0}/{1}]. Check the details errors log", toImport.Item.Location, toImport.Item.Id);
611  continue;
612  }
613 
614  // If one asset in the group is in error, don't try to import the group
615  if (toImportByImporter.HasErrors)
616  {
617  result.Warning("Disable importing asset [{0}/{1}] while other assets in same group are in errors. Check the details errors log", toImport.Item.Location, toImport.Item.Id);
618  continue;
619  }
620 
621  OnProgress(AssetImportSessionEventType.Begin, AssetImportSessionStepType.Importing, toImport);
622  try
623  {
624  AssetItem item;
625 
626  // If there is an asset merged, use it directly
627  if (toImport.MergedItem != null)
628  {
629  item = toImport.MergedItem;
630 
631  var existingItem = session.FindAsset(item.Id);
632  if (existingItem != null && existingItem.Package != null)
633  {
634  assetPackage = existingItem.Package;
635  }
636 
637  // Remove the asset before reimporting it
638  if (assetPackage.Assets.RemoveById(item.Id))
639  {
640  result.RemovedAssets.Add(item.Id);
641  result.AddedAssets.RemoveWhere(x => x.Id == item.Id);
642  }
643  }
644  else
645  {
646  // Else simply use the asset in input
647  item = toImport.SelectedItem ?? toImport.Item;
648 
649  // If the asset we are trying to import is a merged asset that will be imported
650  // don't try to import it
651  if (mergedAssets.Contains(item.Id))
652  {
653  continue;
654  }
655 
656  // Update the name just before adding to the package
657  // to make sure that there will be no clash
658  FixAssetLocation(item, fileToImport.Directory, assetResolver);
659  }
660 
661  assetPackage.Assets.Add(item);
662  result.AddedAssets.Add(item);
663  }
664  finally
665  {
666  OnProgress(AssetImportSessionEventType.End, AssetImportSessionStepType.Importing, toImport);
667  }
668  }
669  }
670  }
671  }
672 
673  /// <summary>
674  /// Resets the current importItem session.
675  /// </summary>
676  public void Reset()
677  {
678  Imports.Clear();
679  sourceFileToAssets.Clear();
680  }
681 
682  /// <summary>
683  /// List all <see cref="AssetItem"/> to import by sorting them for the asset having most dependencies to assets
684  /// having no dependencies. This is then used by <see cref="PrepareMerge"/> to perform a top-down matching of asset
685  /// to import with exisiting assets.
686  /// </summary>
687  /// <returns></returns>
688  private List<AssetToImportMergeGroup> ComputeToImportListSorted()
689  {
690  var toImportListSorted = new List<AssetToImportMergeGroup>();
691 
692  var registeredTypes = new HashSet<Type>();
693  var typeDependencies = new Dictionary<AssetToImportMergeGroup, HashSet<Type>>();
694  foreach (var toImport in AllImports())
695  {
696  var references = AssetReferenceAnalysis.Visit(toImport.Item.Asset);
697  var refTypes = new HashSet<Type>(references.Select(assetLink => assetLink.Reference).OfType<AssetReference>().Select(assetRef => assetRef.Type));
698  // Optimized path, if an asset has no dependencies, directly add it to the sorted list
699  if (refTypes.Count == 0)
700  {
701  toImportListSorted.Add(toImport);
702  registeredTypes.Add(toImport.Item.Asset.GetType());
703  }
704  else
705  {
706  typeDependencies[toImport] = refTypes;
707  }
708  }
709 
710  var typeToRegisters = new HashSet<Type>();
711  while (true)
712  {
713  typeToRegisters.Clear();
714  var toImportTempList = typeDependencies.ToList();
715  foreach (var toImportWithTypes in toImportTempList)
716  {
717  var toImport = toImportWithTypes.Key;
718 
719  var areDependenciesResolved = toImportWithTypes.Value.All(registeredTypes.Contains);
720  if (areDependenciesResolved)
721  {
722  typeDependencies.Remove(toImport);
723  toImportListSorted.Add(toImport);
724  typeToRegisters.Add(toImport.Item.Asset.GetType());
725  }
726  }
727 
728  // If we have not found new dependencies, than exit
729  if (typeToRegisters.Count == 0)
730  {
731  break;
732  }
733 
734  foreach (var newType in typeToRegisters)
735  {
736  registeredTypes.Add(newType);
737  }
738  }
739 
740  // In case there are some import elements not filtered remaining, we should still add them
741  toImportListSorted.AddRange(typeDependencies.Keys);
742 
743  // We need to return a list from assets with most dependencies to asset with less dependencies
744  toImportListSorted.Reverse();
745 
746  return toImportListSorted;
747  }
748 
749  /// <summary>
750  /// Prepares assets for merging by trying to match with existing assets.
751  /// </summary>
752  /// <param name="cancelToken">The cancel token.</param>
753  private void PrepareMerge(CancellationToken? cancelToken = null)
754  {
755  // Presort the assets so that we will make sure to try to match them from top-down (model to texture for example)
756  //
757  // -----------------------------------||---------------------------------------------
758  // Assets being imported || Assets found in the current session
759  // -----------------------------------||---------------------------------------------
760  // - Model1 => - Model0
761  // - Material1 => - Material0
762  // - Texture 1.1 => - Texture 0.1
763  // - Texture 1.2 => - Texture 0.2
764  //
765  var toImportListSorted = ComputeToImportListSorted();
766 
767  // Pass 2) => Calculate matches
768  // We need to calculate matches after pass1, as we could have to match
769  // between assets being imported
770  foreach (var toImport in toImportListSorted)
771  {
772  var toImportByImporter = toImport.Parent;
773  var fileToImport = toImportByImporter.Parent;
774 
775  OnProgress(AssetImportSessionEventType.Begin, AssetImportSessionStepType.Matching, toImport);
776 
777  var assetItem = toImport.Item;
778  var assetImport = assetItem.Asset as AssetImport;
779  if (assetImport == null || toImport.Merges.Count > 0)
780  {
781  continue;
782  }
783 
784  if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
785  {
786  toImport.Log.Warning("Cancellation requested before matching asset [{0}] with location [{1}] ", fileToImport.File, assetItem.Location);
787  return;
788  }
789 
790  var assetType = assetImport.GetType();
791 
792  List<AssetItem> possibleMatches;
793 
794  // When we are explcitly importing an existing asset, we are going to match the import directly and only with it
795  if (toImportByImporter.PreviousItem != null && toImportByImporter.PreviousItem.Asset.GetType() == assetType)
796  {
797  possibleMatches = new List<AssetItem> { toImportByImporter.PreviousItem };
798  }
799  else
800  {
801  // Else gets a list of existing assets that could match, as we are going to match against them.
802  possibleMatches = GetOrCreateAssetsPerInput(assetImport.Source, assetType).Where(item => item.Asset.GetType() == assetType && item != assetItem).ToList();
803  }
804 
805  foreach (var possibleMatchItem in possibleMatches)
806  {
807  // Main method to match assets
808  RecursiveCalculateMatchAndPrepareMerge(toImportByImporter, toImport, possibleMatchItem);
809  }
810 
811  // If there are no merges, just take the original element to importItem
812  if (toImport.Merges.Count == 0)
813  {
814  toImport.SelectedItem = toImport.Item;
815  }
816  }
817 
818  // Order matches by sort order
819  foreach (var toImport in toImportListSorted)
820  {
821  toImport.Merges.Sort((left, right) =>
822  {
823  if ((left.PreviousItem.Package == null && right.PreviousItem.Package != null)
824  || (left.PreviousItem.Package != null && right.PreviousItem.Package == null))
825  {
826  return left.PreviousItem.Package == null ? 1 : -1;
827  }
828 
829  return -left.MatchingFactor.CompareTo(right.MatchingFactor);
830  });
831 
832  OnProgress(AssetImportSessionEventType.End, AssetImportSessionStepType.Matching, toImport);
833  }
834  }
835 
836  private IEnumerable<AssetToImportMergeGroup> AllImports()
837  {
838  return from fileToImport in Imports.Where(it => it.Enabled) from toImportByImporter in fileToImport.ByImporters.Where(it => it.Enabled) from toImport in toImportByImporter.Items.Where(it => it.Enabled) select toImport;
839  }
840 
841  private AssetToImport RegisterImporter(UFile file, Package package, UDirectory directory, IAssetImporter importer, AssetItem previousItem = null)
842  {
843  var previousEntry = Imports.FirstOrDefault(item => item.File == file);
844  if (previousEntry == null)
845  {
846  previousEntry = new AssetToImport(file) { Package = package, Directory = directory};
847  Imports.Add(previousEntry);
848  }
849  // This importer has not been registered yet
850  if (previousEntry.ByImporters.All(byImporter => byImporter.Importer != importer))
851  {
852  int index = 1 + previousEntry.ByImporters.LastIndexOf(x => x.Importer.DisplayRank > importer.DisplayRank);
853  previousEntry.ByImporters.Insert(index, new AssetToImportByImporter(previousEntry, importer, previousItem));
854  }
855 
856  return previousEntry;
857  }
858 
859  private void FixAssetLocation(AssetItem item, UDirectory targetDirectory, AssetResolver assetResolver)
860  {
861  var path = new UFile(targetDirectory, item.Location.GetFileName(), null);
862  UFile newLocation;
863  assetResolver.RegisterLocation(path, out newLocation);
864  item.Location = newLocation;
865  }
866 
867  private void FreezeAssetImport(IAssetImporter importer, AssetItem assetItem, Package package)
868  {
869  // Base guid for assets to importItem must be empty
870  var baseAsset = (Asset)AssetCloner.Clone(assetItem.Asset);
871  var assetImport = assetItem.Asset as AssetImport;
872  baseAsset.Id = Guid.Empty;
873 
874  // If we have an asset import, compute the hash and make sure the base doesn't include any info about sources
875  if (assetImport != null)
876  {
877  var baseAssetImport = (AssetImport)baseAsset;
878  baseAssetImport.SetAsRootImport();
879 
880  baseAssetImport.ImporterId = importer.Id;
881  var assetImportTracked = assetImport as AssetImportTracked;
882  if (assetImportTracked != null)
883  {
884  assetImportTracked.SourceHash = FileVersionManager.Instance.ComputeFileHash(assetImport.Source);
885  }
886  }
887  assetItem.Asset.Base = new AssetBase(baseAsset);
888  }
889 
890  private void ComputeAssetHash(CancellationToken? cancelToken = null)
891  {
892  sourceFileToAssets.Clear();
893  var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
894 
895  // Pass 1) => Prepare imports
896  // - Check for assets with same source
897  // - Fix location
898  foreach (var fileToImport in Imports.Where(it => it.Enabled))
899  {
900  var assetPackage = fileToImport.Package;
901  var assetResolver = AssetResolver.FromPackage(assetPackage);
902 
903  foreach (var import in fileToImport.ByImporters.Where(it => it.Enabled))
904  {
905  foreach (var assetToImport in import.Items.Where(it => it.Enabled))
906  {
907  if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested)
908  {
909  assetToImport.Log.Warning("Cancellation requested before computing hash for asset [{0}] with location [{1}] ", fileToImport.File, assetToImport.Item.Location);
910  return;
911  }
912 
913  OnProgress(AssetImportSessionEventType.Begin, AssetImportSessionStepType.ComputeHash, assetToImport);
914  try
915  {
916  var assetItem = assetToImport.Item;
917 
918  // Fix asset location
919  FixAssetLocation(assetItem, fileToImport.Directory, assetResolver);
920 
921  // Freeze asset importItem and calculate source hash
922  FreezeAssetImport(import.Importer, assetItem, fileToImport.Package);
923 
924  // Create mapping: assetitem => set of similar asset items
925  var assetImport = assetItem.Asset as AssetImport;
926  if (assetImport != null)
927  {
928  // Create mapping: source file => set of asset items
929  RegisterAssetPerInput(assetImport.Source, assetItem);
930 
931  // Add assets from session
932  foreach (var existingAssetItem in session.DependencyManager.FindAssetItemsByInput(assetImport.Source))
933  {
934  // Filter and only take current root imports
935  if (existingAssetItem.Asset.Base == null || !existingAssetItem.Asset.Base.IsRootImport)
936  {
937  continue;
938  }
939 
940  RegisterAssetPerInput(assetImport.Source, existingAssetItem);
941  }
942  }
943  }
944  finally
945  {
946  OnProgress(AssetImportSessionEventType.End, AssetImportSessionStepType.ComputeHash, assetToImport);
947  }
948  }
949  }
950  }
951  }
952 
953  /// <summary>
954  /// Recursively calculate the match between assets and prepare merge. See remarks.
955  /// </summary>
956  /// <param name="toImport">To import.</param>
957  /// <param name="toImportMergeGroup">To import merge group.</param>
958  /// <param name="previous">The previous.</param>
959  /// <remarks>
960  /// This method is a tricky part of merging assets, as it is able to match assets with existing asset (either from the
961  /// current import session or from the current package session). The main problem is in the case of an asset being
962  /// imported that is an <see cref="AssetImport"/> that is referencing some assets being imported as well that are not
963  /// <see cref="AssetImport"/>. In this case, these referenced assets cannot be detected directly when importing assets.
964  /// But we still need to be able to match these assets with some existing assets.
965  /// In order to detect these references, we are previewing a merge between the <see cref="AssetImport"/> and the asset
966  /// in the current session. In the differences between them, we are handling specially differences for
967  /// <see cref="IContentReference"/> as we expect them to be remapped by the importing process.
968  ///
969  /// For example, suppose a package contains a model A from a specified FBX file that is referencing assets B1 and B2.
970  /// When we are trying to import the same model A, it will first create A' that will reference B1' and B2'.
971  /// It is easy to find that correspondance between A and A', as they have the same FBX source file. But for B1/B1' or
972  /// B2/B2', we need to first check that this references are broken, associate that B1' is in fact B1, B2' is in fact
973  /// B2, and try to merge this assets recursively (as they could also contains references to other assets)
974  ///
975  /// TODO: Review this comment by someone else!
976  /// </remarks>
977  private void RecursiveCalculateMatchAndPrepareMerge(AssetToImportByImporter toImport, AssetToImportMergeGroup toImportMergeGroup, AssetItem previous)
978  {
979  var newAssetBase = toImportMergeGroup.Item.Asset.Base.Asset;
980  var previousAsset = previous.Asset;
981  var previousBase = previous.Asset.Base == null || previous.Asset.Base.Asset == null ? newAssetBase : previous.Asset.Base.Asset;
982 
983  // If this matching has been already processed, exit immediately
984  if (toImportMergeGroup.Merges.Any(matching => matching.PreviousItem.Id == previous.Id))
985  {
986  return;
987  }
988 
989  // If the new asset is an asset import, we need to copy the freshly computed SourceHash to it.
990  var newAsset = (Asset)AssetCloner.Clone(previousAsset);
991  var newAssetImport = newAsset as AssetImportTracked;
992  if (newAssetImport != null)
993  {
994  var originAssetImport = (AssetImportTracked)toImportMergeGroup.Item.Asset;
995  newAssetImport.SourceHash = originAssetImport.SourceHash;
996  }
997 
998  // Make a diff between the previous asset and the new asset to importItem
999  var assetDiff = new AssetDiff(previousBase, newAsset, newAssetBase);
1000 
1001  // Perform a preview merge
1002  var result = AssetMerge.Merge(assetDiff, MergeImportPolicy, true);
1003 
1004  // Retrieve the precalculated list of diffs
1005  var diff3 = assetDiff.Compute();
1006 
1007  var totalChildren = diff3.CountChildren();
1008  var diffList = diff3.FindDifferences().ToList();
1009 
1010  var conflictCount = diffList.Count(node => node.HasConflict);
1011 
1012  // Gets the references differences
1013  var assetReferencesDiffs = diffList.Where(node => typeof(IContentReference).IsAssignableFrom(node.InstanceType)).ToList();
1014 
1015  // The matching is calculated taking into account the number of conflicts and the number
1016  // of unresolved references (implicit conflicts)
1017  var assetMatching = new AssetToImportMerge(previous, assetDiff, result);
1018 
1019  // The number of references
1020  var subReferenceCount = assetReferencesDiffs.Count;
1021 
1022  // The number of references whose their content exist
1023  var subReferenceMatch = 0;
1024 
1025  // Recursively calculate differences on referenced objects
1026  foreach (var referenceDiff in assetReferencesDiffs)
1027  {
1028  var base1 = (IContentReference)referenceDiff.BaseNode.Instance;
1029  var newRef = (IContentReference)referenceDiff.Asset2Node.Instance;
1030 
1031  if (base1 != null && newRef != null)
1032  {
1033  // Check if the referenced asset is existing in the session
1034  var baseItem1 = session.FindAsset(base1.Id);
1035  if (baseItem1 != null)
1036  {
1037  // Try to find an asset from the import session that is matching
1038  var subImport1 = toImport.Items.Where(it => it.Enabled).FirstOrDefault(importList => importList.Item.Id == newRef.Id);
1039  if (subImport1 != null && baseItem1.Asset.GetType() == subImport1.Item.Asset.GetType())
1040  {
1041  RecursiveCalculateMatchAndPrepareMerge(toImport, subImport1, baseItem1);
1042 
1043  // Update dependencies so we will be able to sort the matching in the Merge() method.
1044  assetMatching.DependencyGroups.Add(subImport1);
1045  subReferenceMatch++;
1046  }
1047  }
1048  }
1049  else if (base1 == null && newRef != null)
1050  {
1051  subReferenceMatch++;
1052  }
1053  }
1054 
1055  // Calculate a matching factor
1056  // In the standard case, we should have subReferenceCount == subReferenceMatch
1057  assetMatching.MatchingFactor = 1.0 - (double)(conflictCount + subReferenceCount - subReferenceMatch)/totalChildren;
1058 
1059  toImportMergeGroup.Merges.Add(assetMatching);
1060  }
1061 
1062  private Diff3ChangeType MergeImportPolicy(Diff3Node node)
1063  {
1064  // Asset references are a special case while importing, as we are
1065  // going to try to rematch them, so if they changed, we expect to
1066  // use the original instance
1067  if (typeof(IContentReference).IsAssignableFrom(node.InstanceType))
1068  {
1069  if (node.Asset1Node != null)
1070  {
1071  return Diff3ChangeType.MergeFromAsset1;
1072  }
1073 
1074  if (node.Asset2Node != null)
1075  {
1076  return Diff3ChangeType.MergeFromAsset2;
1077  }
1078  }
1079  return AssetMergePolicies.MergePolicyAsset2AsNewBaseOfAsset1(node);
1080  }
1081 
1082  private void RegisterAssetPerInput(string rawSourcePath, AssetItem assetItem)
1083  {
1084  GetOrCreateAssetsPerInput(rawSourcePath, assetItem.Asset.GetType()).Add(assetItem);
1085  }
1086 
1087  private HashSet<AssetItem> GetOrCreateAssetsPerInput(string rawSourcePath, Type assetType)
1088  {
1089  var assetKey = new AssetLocationTyped(rawSourcePath, assetType);
1090  HashSet<AssetItem> assetsPerFile;
1091  if (!sourceFileToAssets.TryGetValue(assetKey, out assetsPerFile))
1092  {
1093  assetsPerFile = new HashSet<AssetItem>(AssetItem.DefaultComparerById);
1094  sourceFileToAssets.Add(assetKey, assetsPerFile);
1095  }
1096  return assetsPerFile;
1097  }
1098 
1099  /// <summary>
1100  /// This method is validating all assets being imported. If there is any errors,
1101  /// it is throwing an <see cref="InvalidOperationException"/> as this is considered as an invalid usage of the API.
1102  /// </summary>
1103  /// <param name="assets"></param>
1104  /// <param name="ids"></param>
1105  private void CheckAssetsToImport(IEnumerable<AssetItem> assets, HashSet<Guid> ids)
1106  {
1107  // Pre-check
1108  var log = new LoggerResult();
1109  bool hasAssetImport = false;
1110  foreach (var assetItem in assets)
1111  {
1112  if (assetItem.Id == Guid.Empty)
1113  {
1114  log.Error("Invalid arguments while importing asset [{0}]. Requiring an asset Id not empty", assetItem);
1115  }
1116  else if (ids.Contains(assetItem.Id))
1117  {
1118  log.Error("Invalid arguments while importing asset [{0}]. An asset is already being imported with the same id", assetItem);
1119  }
1120  else
1121  {
1122  var existingAssetItem = session.FindAsset(assetItem.Id);
1123  if (existingAssetItem != null)
1124  {
1125  log.Error("Invalid arguments while importing asset [{0}]. An asset is already used by the package [{1}/{2}] in the current session", assetItem, existingAssetItem.Package.Id, existingAssetItem.Package.FullPath);
1126  }
1127  }
1128 
1129  if (assetItem.Asset.Base != null)
1130  {
1131  log.Error("Invalid arguments while importing asset [{0}]. Base must be null", assetItem);
1132  }
1133 
1134  var assetImport = assetItem.Asset as AssetImport;
1135  if (assetImport != null)
1136  {
1137  hasAssetImport = true;
1138  if (assetImport.Source == null || !assetImport.Source.IsAbsolute)
1139  {
1140  log.Error("Invalid arguments while importing asset [{0}]. Type [{1}] cannot be null and must be an absolute location", assetItem, assetImport.Source);
1141  }
1142  }
1143 
1144  ids.Add(assetItem.Id);
1145  }
1146 
1147  // Check that there is at least an AssetImport
1148  if (!hasAssetImport)
1149  {
1150  log.Error("Error expecting at least one AssetImport while importing assets to a package");
1151  }
1152 
1153  // If we have any errors, don't process further
1154  if (log.HasErrors)
1155  {
1156  // Genegerate an exception as all checks above are supposed to be an invalid usage of the API
1157  throw new InvalidOperationException("Unexpected error while processing items to importItem: " + log.ToText());
1158  }
1159  }
1160 
1161  [DebuggerDisplay("Location: {location}")]
1162  private struct AssetLocationTyped : IEquatable<AssetLocationTyped>
1163  {
1164  public AssetLocationTyped(string location, Type assetType)
1165  {
1166  this.location = location;
1167  this.assetType = assetType;
1168  }
1169 
1170  private readonly string location;
1171 
1172  private readonly Type assetType;
1173 
1174  public bool Equals(AssetLocationTyped other)
1175  {
1176  return string.Equals(location, other.location, StringComparison.OrdinalIgnoreCase) && assetType == other.assetType;
1177  }
1178 
1179  public override bool Equals(object obj)
1180  {
1181  if (ReferenceEquals(null, obj)) return false;
1182  return obj is AssetLocationTyped && Equals((AssetLocationTyped)obj);
1183  }
1184 
1185  public override int GetHashCode()
1186  {
1187  unchecked
1188  {
1189  return (location.GetHashCode()*397) ^ assetType.GetHashCode();
1190  }
1191  }
1192  }
1193 
1194  private void OnProgress(AssetImportSessionEventType step, AssetImportSessionStepType type, AssetToImportByImporter toImportByImporter)
1195  {
1196  EventHandler<AssetImportSessionEvent> handler = Progress;
1197  if (handler != null) handler(this, new AssetImportSessionEvent(step, type, toImportByImporter));
1198  }
1199 
1200  private void OnProgress(AssetImportSessionEventType step, AssetImportSessionStepType type, AssetToImportMergeGroup toImportMergeGroup)
1201  {
1202  EventHandler<AssetImportSessionEvent> handler = Progress;
1203  if (handler != null) handler(this, new AssetImportSessionEvent(step, type, toImportMergeGroup));
1204  }
1205  }
1206 }
AssetImportSessionEventType
The type of event begin or end published by AssetImportSession.Progress
Type InstanceType
Gets or sets the type of the instance. Null if instance type is different between the nodes...
Definition: Diff3Node.cs:40
Keys
Enumeration for keys.
Definition: Keys.cs:8
Base class for Asset.
Definition: Asset.cs:14
Helper to find available new asset locations and identifiers.
SiliconStudio.Core.Diagnostics.LoggerResult LoggerResult
Guid Id
Gets the unique identifier of this asset.
Definition: AssetItem.cs:85
bool IsExistingAssetForReImportSupported(AssetItem assetItem)
Determines whether the specified asset is supporting re-import.
bool Stage(CancellationToken?cancelToken=null)
Analyze files for preparing them for the merge and import steps. This must be called first after call...
The template can be applied to an existing Assets.Package.
AssetImportSession(PackageSession session)
Initializes a new instance of the AssetImportSession class.
An asset item part of a Package accessible through SiliconStudio.Assets.Package.Assets.
Definition: AssetItem.cs:17
AssetToImport AddFile(UFile file, IAssetImporter importer, Package package, UDirectory directory)
Adds a file to import.
Describes an asset to import associated with possible existing assets, mergeable or not...
string GetFileExtension()
Gets the extension of the file. Can be null.
Definition: UFile.cs:84
Let the emitter choose the style.
A session for editing a package.
Package Package
Gets the package where this asset is stored.
Definition: AssetItem.cs:97
AssetToImport AddExistingAssetForReImport(AssetItem assetItem)
Adds an existing asset for reimport
EventHandler< AssetImportSessionEvent > Progress
Occurs when this import session is making progress.
void Merge(CancellationToken?cancelToken=null)
Merges each asset with the selected asset specified in AssetToImportMergeGroup.SelectedItem ...
Defines a normalized directory path. See UPath for details. This class cannot be inherited.
Definition: UDirectory.cs:13
A raw asset being imported that will generate possibly multiple AssetItem
PackageSession Session
Gets the session.
Definition: Package.cs:219
bool IsAbsolute
Determines whether this instance is absolute.
Definition: UPath.cs:269
System.IO.File File
string Name
Gets the name of this importer.
A logger that stores added and removed assets of an import operation.
Definition: ImportResult.cs:13
AssetToImport AddFile(UFile file, Package package, UDirectory directory)
Adds a file to import.
The type of the serialized type will be passed as a generic arguments of the serializer. Example: serializer of A becomes instantiated as Serializer{A}.
An importable asset with a content that need to be tracked if original asset is changing.
UFile Location
Gets the location of this asset.
Definition: AssetItem.cs:51
void Reset()
Resets the current importItem session.
string GetDirectory()
Gets the directory. Can be null.
Definition: UPath.cs:187
void Import(ImportResult result)
Imports all assets
IEnumerable< AssetToImport > AddFiles(IEnumerable< UFile > files, Package package, UDirectory directory)
Adds files to import.
This class is handling importItem of assets into a session. See remarks for usage.
bool IsFileSupported(UFile file)
Determines whether the specified file is supported
ImportResult Import()
Imports all assets
Imports a raw asset into the asset system.
Class AssetDiff. This class cannot be inherited.
Definition: AssetDiff.cs:15
A package managing assets.
Definition: Package.cs:28
An interface that provides a reference to an asset.
AssetImportSessionStepType
The step being processed by the AssetImportSession
Defines a normalized file path. See UPath for details. This class cannot be inherited.
Definition: UFile.cs:13