PageRenderTime 1622ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/MonoGame.Framework.Content.Pipeline/Builder/PipelineManager.cs

https://github.com/dmanning23/MonoGame
C# | 940 lines | 652 code | 145 blank | 143 comment | 109 complexity | d0ced5526d1169bc349752e249a657b5 MD5 | raw file
Possible License(s): CC-BY-SA-3.0
  1. // MonoGame - Copyright (C) The MonoGame Team
  2. // This file is subject to the terms and conditions defined in
  3. // file 'LICENSE.txt', which is part of this source code package.
  4. using System;
  5. using System.Collections.Generic;
  6. using System.ComponentModel;
  7. using System.IO;
  8. using System.Linq;
  9. using System.Reflection;
  10. using Microsoft.Xna.Framework.Content.Pipeline;
  11. using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
  12. using Microsoft.Xna.Framework.Graphics;
  13. using System.Globalization;
  14. using Microsoft.Xna.Framework.Content.Pipeline.Builder.Convertors;
  15. using System.Diagnostics;
  16. namespace MonoGame.Framework.Content.Pipeline.Builder
  17. {
  18. public class PipelineManager
  19. {
  20. [DebuggerDisplay("ImporterInfo: {type.Name}")]
  21. private struct ImporterInfo
  22. {
  23. public ContentImporterAttribute attribute;
  24. public Type type;
  25. public DateTime assemblyTimestamp;
  26. };
  27. private List<ImporterInfo> _importers;
  28. [DebuggerDisplay("ProcessorInfo: {type.Name}")]
  29. private struct ProcessorInfo
  30. {
  31. public ContentProcessorAttribute attribute;
  32. public Type type;
  33. public DateTime assemblyTimestamp;
  34. };
  35. private List<ProcessorInfo> _processors;
  36. private List<Type> _writers;
  37. // Keep track of all built assets. (Required to resolve automatic names "AssetName_n".)
  38. // Key = absolute, normalized path of source file
  39. // Value = list of build events
  40. // (Note: When using external references, an asset may be built multiple times
  41. // with different parameters.)
  42. private readonly Dictionary<string, List<PipelineBuildEvent>> _pipelineBuildEvents;
  43. // Store default values for content processor parameters. (Necessary to compare processor
  44. // parameters. See PipelineBuildEvent.AreParametersEqual.)
  45. // Key = name of content processor
  46. // Value = processor parameters
  47. private readonly Dictionary<string, OpaqueDataDictionary> _processorDefaultValues;
  48. public string ProjectDirectory { get; private set; }
  49. public string OutputDirectory { get; private set; }
  50. public string IntermediateDirectory { get; private set; }
  51. public ContentStatsCollection ContentStats { get; private set; }
  52. private ContentCompiler _compiler;
  53. public ContentBuildLogger Logger { get; set; }
  54. public List<string> Assemblies { get; private set; }
  55. /// <summary>
  56. /// The current target graphics profile for which all content is built.
  57. /// </summary>
  58. public GraphicsProfile Profile { get; set; }
  59. /// <summary>
  60. /// The current target platform for which all content is built.
  61. /// </summary>
  62. public TargetPlatform Platform { get; set; }
  63. /// <summary>
  64. /// The build configuration passed thru to content processors.
  65. /// </summary>
  66. public string Config { get; set; }
  67. /// <summary>
  68. /// Gets or sets if the content is compressed.
  69. /// </summary>
  70. public bool CompressContent { get; set; }
  71. /// <summary>
  72. /// If true exceptions thrown from within an importer or processor are caught and then
  73. /// thrown from the context. Default value is true.
  74. /// </summary>
  75. public bool RethrowExceptions { get; set; }
  76. public PipelineManager(string projectDir, string outputDir, string intermediateDir)
  77. {
  78. _pipelineBuildEvents = new Dictionary<string, List<PipelineBuildEvent>>();
  79. _processorDefaultValues = new Dictionary<string, OpaqueDataDictionary>();
  80. RethrowExceptions = true;
  81. Assemblies = new List<string>();
  82. Assemblies.Add(null);
  83. Logger = new PipelineBuildLogger();
  84. ProjectDirectory = PathHelper.NormalizeDirectory(projectDir);
  85. OutputDirectory = PathHelper.NormalizeDirectory(outputDir);
  86. IntermediateDirectory = PathHelper.NormalizeDirectory(intermediateDir);
  87. RegisterCustomConverters();
  88. // Load the previous content stats.
  89. ContentStats = new ContentStatsCollection();
  90. ContentStats.PreviousStats = ContentStatsCollection.Read(intermediateDir);
  91. }
  92. public void AssignTypeConverter<TType, TTypeConverter> ()
  93. {
  94. TypeDescriptor.AddAttributes (typeof (TType), new TypeConverterAttribute (typeof (TTypeConverter)));
  95. }
  96. private void RegisterCustomConverters ()
  97. {
  98. AssignTypeConverter<Microsoft.Xna.Framework.Color, StringToColorConverter> ();
  99. }
  100. public void AddAssembly(string assemblyFilePath)
  101. {
  102. if (assemblyFilePath == null)
  103. throw new ArgumentException("assemblyFilePath cannot be null!");
  104. if (!Path.IsPathRooted(assemblyFilePath))
  105. throw new ArgumentException("assemblyFilePath must be absolute!");
  106. // Make sure we're not adding the same assembly twice.
  107. assemblyFilePath = PathHelper.Normalize(assemblyFilePath);
  108. if (!Assemblies.Contains(assemblyFilePath))
  109. {
  110. Assemblies.Add(assemblyFilePath);
  111. //TODO need better way to update caches
  112. _processors = null;
  113. _importers = null;
  114. _writers = null;
  115. }
  116. }
  117. private void ResolveAssemblies()
  118. {
  119. _importers = new List<ImporterInfo>();
  120. _processors = new List<ProcessorInfo>();
  121. _writers = new List<Type>();
  122. // Finally load the pipeline assemblies.
  123. foreach (var assemblyPath in Assemblies)
  124. {
  125. Type[] exportedTypes;
  126. DateTime assemblyTimestamp;
  127. try
  128. {
  129. Assembly a;
  130. if (string.IsNullOrEmpty(assemblyPath))
  131. a = Assembly.GetExecutingAssembly();
  132. else
  133. a = Assembly.LoadFrom(assemblyPath);
  134. exportedTypes = a.GetTypes();
  135. assemblyTimestamp = File.GetLastWriteTime(a.Location);
  136. }
  137. catch (BadImageFormatException e)
  138. {
  139. Logger.LogWarning(null, null, "Assembly is either corrupt or built using a different " +
  140. "target platform than this process. Reference another target architecture (x86, x64, " +
  141. "AnyCPU, etc.) of this assembly. '{0}': {1}", assemblyPath, e.Message);
  142. // The assembly failed to load... nothing
  143. // we can do but ignore it.
  144. continue;
  145. }
  146. catch (Exception e)
  147. {
  148. Logger.LogWarning(null, null, "Failed to load assembly '{0}': {1}", assemblyPath, e.Message);
  149. continue;
  150. }
  151. foreach (var t in exportedTypes)
  152. {
  153. if (t.IsAbstract)
  154. continue;
  155. if (t.GetInterface(@"IContentImporter") != null)
  156. {
  157. var attributes = t.GetCustomAttributes(typeof (ContentImporterAttribute), false);
  158. if (attributes.Length != 0)
  159. {
  160. var importerAttribute = attributes[0] as ContentImporterAttribute;
  161. _importers.Add(new ImporterInfo
  162. {
  163. attribute = importerAttribute,
  164. type = t,
  165. assemblyTimestamp = assemblyTimestamp
  166. });
  167. }
  168. else
  169. {
  170. // If no attribute specify default one
  171. var importerAttribute = new ContentImporterAttribute(".*");
  172. importerAttribute.DefaultProcessor = "";
  173. importerAttribute.DisplayName = t.Name;
  174. _importers.Add(new ImporterInfo
  175. {
  176. attribute = importerAttribute,
  177. type = t,
  178. assemblyTimestamp = assemblyTimestamp
  179. });
  180. }
  181. }
  182. else if (t.GetInterface(@"IContentProcessor") != null)
  183. {
  184. var attributes = t.GetCustomAttributes(typeof (ContentProcessorAttribute), false);
  185. if (attributes.Length != 0)
  186. {
  187. var processorAttribute = attributes[0] as ContentProcessorAttribute;
  188. _processors.Add(new ProcessorInfo
  189. {
  190. attribute = processorAttribute,
  191. type = t,
  192. assemblyTimestamp = assemblyTimestamp
  193. });
  194. }
  195. }
  196. else if (t.GetInterface(@"ContentTypeWriter") != null)
  197. {
  198. // TODO: This doesn't work... how do i find these?
  199. _writers.Add(t);
  200. }
  201. }
  202. }
  203. }
  204. public Type[] GetImporterTypes()
  205. {
  206. if (_importers == null)
  207. ResolveAssemblies();
  208. List<Type> types = new List<Type>();
  209. foreach (var item in _importers)
  210. {
  211. types.Add(item.type);
  212. }
  213. return types.ToArray();
  214. }
  215. public Type[] GetProcessorTypes()
  216. {
  217. if (_processors == null)
  218. ResolveAssemblies();
  219. List<Type> types = new List<Type>();
  220. foreach (var item in _processors)
  221. {
  222. types.Add(item.type);
  223. }
  224. return types.ToArray();
  225. }
  226. public IContentImporter CreateImporter(string name)
  227. {
  228. if (_importers == null)
  229. ResolveAssemblies();
  230. // Search for the importer.
  231. foreach (var info in _importers)
  232. {
  233. if (info.type.Name.Equals(name))
  234. return Activator.CreateInstance(info.type) as IContentImporter;
  235. }
  236. return null;
  237. }
  238. public string FindImporterByExtension(string ext)
  239. {
  240. if (_importers == null)
  241. ResolveAssemblies();
  242. // Search for the importer.
  243. foreach (var info in _importers)
  244. {
  245. if (info.attribute.FileExtensions.Any(e => e.Equals(ext, StringComparison.InvariantCultureIgnoreCase)))
  246. return info.type.Name;
  247. }
  248. return null;
  249. }
  250. public DateTime GetImporterAssemblyTimestamp(string name)
  251. {
  252. if (_importers == null)
  253. ResolveAssemblies();
  254. // Search for the importer.
  255. foreach (var info in _importers)
  256. {
  257. if (info.type.Name.Equals(name))
  258. return info.assemblyTimestamp;
  259. }
  260. return DateTime.MaxValue;
  261. }
  262. public string FindDefaultProcessor(string importer)
  263. {
  264. if (_importers == null)
  265. ResolveAssemblies();
  266. // Search for the importer.
  267. foreach (var info in _importers)
  268. {
  269. if (info.type.Name == importer)
  270. return info.attribute.DefaultProcessor;
  271. }
  272. return null;
  273. }
  274. public Type GetProcessorType(string name)
  275. {
  276. if (_processors == null)
  277. ResolveAssemblies();
  278. // Search for the processor type.
  279. foreach (var info in _processors)
  280. {
  281. if (info.type.Name.Equals(name))
  282. return info.type;
  283. }
  284. return null;
  285. }
  286. public void ResolveImporterAndProcessor(string sourceFilepath, ref string importerName, ref string processorName)
  287. {
  288. // Resolve the importer name.
  289. if (string.IsNullOrEmpty(importerName))
  290. importerName = FindImporterByExtension(Path.GetExtension(sourceFilepath));
  291. if (string.IsNullOrEmpty(importerName))
  292. throw new Exception(string.Format("Couldn't find a default importer for '{0}'!", sourceFilepath));
  293. // Resolve the processor name.
  294. if (string.IsNullOrEmpty(processorName))
  295. processorName = FindDefaultProcessor(importerName);
  296. if (string.IsNullOrEmpty(processorName))
  297. throw new Exception(string.Format("Couldn't find a default processor for importer '{0}'!", importerName));
  298. }
  299. public IContentProcessor CreateProcessor(string name, OpaqueDataDictionary processorParameters)
  300. {
  301. var processorType = GetProcessorType(name);
  302. if (processorType == null)
  303. return null;
  304. // Create the processor.
  305. var processor = (IContentProcessor)Activator.CreateInstance(processorType);
  306. // Convert and set the parameters on the processor.
  307. foreach (var param in processorParameters)
  308. {
  309. var propInfo = processorType.GetProperty(param.Key, BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);
  310. if (propInfo == null || propInfo.GetSetMethod(false) == null)
  311. continue;
  312. // If the property value is already of the correct type then set it.
  313. if (propInfo.PropertyType.IsInstanceOfType(param.Value))
  314. propInfo.SetValue(processor, param.Value, null);
  315. else
  316. {
  317. // Find a type converter for this property.
  318. var typeConverter = TypeDescriptor.GetConverter(propInfo.PropertyType);
  319. if (typeConverter.CanConvertFrom(param.Value.GetType()))
  320. {
  321. var propValue = typeConverter.ConvertFrom(null, CultureInfo.InvariantCulture, param.Value);
  322. propInfo.SetValue(processor, propValue, null);
  323. }
  324. }
  325. }
  326. return processor;
  327. }
  328. /// <summary>
  329. /// Gets the default values for the content processor parameters.
  330. /// </summary>
  331. /// <param name="processorName">The name of the content processor.</param>
  332. /// <returns>
  333. /// A dictionary containing the default value for each parameter. Returns
  334. /// <see langword="null"/> if the content processor has not been created yet.
  335. /// </returns>
  336. public OpaqueDataDictionary GetProcessorDefaultValues(string processorName)
  337. {
  338. // null is not allowed as key in dictionary.
  339. if (processorName == null)
  340. processorName = string.Empty;
  341. OpaqueDataDictionary defaultValues;
  342. if (!_processorDefaultValues.TryGetValue(processorName, out defaultValues))
  343. {
  344. // Create the content processor instance and read the default values.
  345. defaultValues = new OpaqueDataDictionary();
  346. var processorType = GetProcessorType(processorName);
  347. if (processorType != null)
  348. {
  349. try
  350. {
  351. var processor = (IContentProcessor)Activator.CreateInstance(processorType);
  352. var properties = processorType.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);
  353. foreach (var property in properties)
  354. defaultValues.Add(property.Name, property.GetValue(processor, null));
  355. }
  356. catch
  357. {
  358. // Ignore exception. Will be handled in ProcessContent.
  359. }
  360. }
  361. _processorDefaultValues.Add(processorName, defaultValues);
  362. }
  363. return defaultValues;
  364. }
  365. public DateTime GetProcessorAssemblyTimestamp(string name)
  366. {
  367. if (_processors == null)
  368. ResolveAssemblies();
  369. // Search for the processor.
  370. foreach (var info in _processors)
  371. {
  372. if (info.type.Name.Equals(name))
  373. return info.assemblyTimestamp;
  374. }
  375. return DateTime.MaxValue;
  376. }
  377. public OpaqueDataDictionary ValidateProcessorParameters(string name, OpaqueDataDictionary processorParameters)
  378. {
  379. var result = new OpaqueDataDictionary();
  380. var processorType = GetProcessorType(name);
  381. if (processorType == null || processorParameters == null)
  382. {
  383. return result;
  384. }
  385. foreach (var param in processorParameters)
  386. {
  387. var propInfo = processorType.GetProperty(param.Key, BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);
  388. if (propInfo == null || propInfo.GetSetMethod(false) == null)
  389. continue;
  390. // Make sure we can assign the value.
  391. if (!propInfo.PropertyType.IsInstanceOfType(param.Value))
  392. {
  393. // Make sure we can convert the value.
  394. var typeConverter = TypeDescriptor.GetConverter(propInfo.PropertyType);
  395. if (!typeConverter.CanConvertFrom(param.Value.GetType()))
  396. continue;
  397. }
  398. result.Add(param.Key, param.Value);
  399. }
  400. return result;
  401. }
  402. private void ResolveOutputFilepath(string sourceFilepath, ref string outputFilepath)
  403. {
  404. // If the output path is null... build it from the source file path.
  405. if (string.IsNullOrEmpty(outputFilepath))
  406. {
  407. var filename = Path.GetFileNameWithoutExtension(sourceFilepath) + ".xnb";
  408. var directory = PathHelper.GetRelativePath(ProjectDirectory,
  409. Path.GetDirectoryName(sourceFilepath) +
  410. Path.DirectorySeparatorChar);
  411. outputFilepath = Path.Combine(OutputDirectory, directory, filename);
  412. }
  413. else
  414. {
  415. // If the extension is not XNB or the source file extension then add XNB.
  416. var sourceExt = Path.GetExtension(sourceFilepath);
  417. if (outputFilepath.EndsWith(sourceExt, StringComparison.InvariantCultureIgnoreCase))
  418. outputFilepath = outputFilepath.Substring(0, outputFilepath.Length - sourceExt.Length);
  419. if (!outputFilepath.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase))
  420. outputFilepath += ".xnb";
  421. // If the path isn't rooted then put it into the output directory.
  422. if (!Path.IsPathRooted(outputFilepath))
  423. outputFilepath = Path.Combine(OutputDirectory, outputFilepath);
  424. }
  425. outputFilepath = PathHelper.Normalize(outputFilepath);
  426. }
  427. private PipelineBuildEvent LoadBuildEvent(string destFile, out string eventFilepath)
  428. {
  429. var contentPath = Path.ChangeExtension(PathHelper.GetRelativePath(OutputDirectory, destFile), PipelineBuildEvent.Extension);
  430. eventFilepath = Path.Combine(IntermediateDirectory, contentPath);
  431. return PipelineBuildEvent.Load(eventFilepath);
  432. }
  433. public void RegisterContent(string sourceFilepath, string outputFilepath = null, string importerName = null, string processorName = null, OpaqueDataDictionary processorParameters = null)
  434. {
  435. sourceFilepath = PathHelper.Normalize(sourceFilepath);
  436. ResolveOutputFilepath(sourceFilepath, ref outputFilepath);
  437. ResolveImporterAndProcessor(sourceFilepath, ref importerName, ref processorName);
  438. var contentEvent = new PipelineBuildEvent
  439. {
  440. SourceFile = sourceFilepath,
  441. DestFile = outputFilepath,
  442. Importer = importerName,
  443. Processor = processorName,
  444. Parameters = ValidateProcessorParameters(processorName, processorParameters),
  445. };
  446. // Register pipeline build event. (Required to correctly resolve external dependencies.)
  447. TrackPipelineBuildEvent(contentEvent);
  448. }
  449. public PipelineBuildEvent BuildContent(string sourceFilepath, string outputFilepath = null, string importerName = null, string processorName = null, OpaqueDataDictionary processorParameters = null)
  450. {
  451. sourceFilepath = PathHelper.Normalize(sourceFilepath);
  452. ResolveOutputFilepath(sourceFilepath, ref outputFilepath);
  453. ResolveImporterAndProcessor(sourceFilepath, ref importerName, ref processorName);
  454. // Record what we're building and how.
  455. var contentEvent = new PipelineBuildEvent
  456. {
  457. SourceFile = sourceFilepath,
  458. DestFile = outputFilepath,
  459. Importer = importerName,
  460. Processor = processorName,
  461. Parameters = ValidateProcessorParameters(processorName, processorParameters),
  462. };
  463. // Load the previous content event if it exists.
  464. string eventFilepath;
  465. var cachedEvent = LoadBuildEvent(contentEvent.DestFile, out eventFilepath);
  466. BuildContent(contentEvent, cachedEvent, eventFilepath);
  467. return contentEvent;
  468. }
  469. private void BuildContent(PipelineBuildEvent pipelineEvent, PipelineBuildEvent cachedEvent, string eventFilepath)
  470. {
  471. if (!File.Exists(pipelineEvent.SourceFile))
  472. {
  473. Logger.LogMessage("{0}", pipelineEvent.SourceFile);
  474. throw new PipelineException("The source file '{0}' does not exist!", pipelineEvent.SourceFile);
  475. }
  476. Logger.PushFile(pipelineEvent.SourceFile);
  477. // Keep track of all build events. (Required to resolve automatic names "AssetName_n".)
  478. TrackPipelineBuildEvent(pipelineEvent);
  479. var rebuild = pipelineEvent.NeedsRebuild(this, cachedEvent);
  480. if (rebuild)
  481. Logger.LogMessage("{0}", pipelineEvent.SourceFile);
  482. else
  483. Logger.LogMessage("Skipping {0}", pipelineEvent.SourceFile);
  484. Logger.Indent();
  485. try
  486. {
  487. if (!rebuild)
  488. {
  489. // While this asset doesn't need to be rebuilt the dependent assets might.
  490. foreach (var asset in cachedEvent.BuildAsset)
  491. {
  492. string assetEventFilepath;
  493. var assetCachedEvent = LoadBuildEvent(asset, out assetEventFilepath);
  494. // If we cannot find the cached event for the dependancy
  495. // then we have to trigger a rebuild of the parent content.
  496. if (assetCachedEvent == null)
  497. {
  498. rebuild = true;
  499. break;
  500. }
  501. var depEvent = new PipelineBuildEvent
  502. {
  503. SourceFile = assetCachedEvent.SourceFile,
  504. DestFile = assetCachedEvent.DestFile,
  505. Importer = assetCachedEvent.Importer,
  506. Processor = assetCachedEvent.Processor,
  507. Parameters = assetCachedEvent.Parameters,
  508. };
  509. // Give the asset a chance to rebuild.
  510. BuildContent(depEvent, assetCachedEvent, assetEventFilepath);
  511. }
  512. }
  513. // Do we need to rebuild?
  514. if (rebuild)
  515. {
  516. var startTime = DateTime.UtcNow;
  517. // Import and process the content.
  518. var processedObject = ProcessContent(pipelineEvent);
  519. // Write the content to disk.
  520. WriteXnb(processedObject, pipelineEvent);
  521. // Store the timestamp of the DLLs containing the importer and processor.
  522. pipelineEvent.ImporterTime = GetImporterAssemblyTimestamp(pipelineEvent.Importer);
  523. pipelineEvent.ProcessorTime = GetProcessorAssemblyTimestamp(pipelineEvent.Processor);
  524. // Store the new event into the intermediate folder.
  525. pipelineEvent.Save(eventFilepath);
  526. var buildTime = DateTime.UtcNow - startTime;
  527. // Record stat for this file.
  528. ContentStats.RecordStats(pipelineEvent.SourceFile, pipelineEvent.DestFile, pipelineEvent.Processor, processedObject.GetType(), (float)buildTime.TotalSeconds);
  529. }
  530. else
  531. {
  532. // Copy the stats from the previous build.
  533. ContentStats.CopyPreviousStats(pipelineEvent.SourceFile);
  534. }
  535. }
  536. finally
  537. {
  538. Logger.Unindent();
  539. Logger.PopFile();
  540. }
  541. }
  542. public object ProcessContent(PipelineBuildEvent pipelineEvent)
  543. {
  544. if (!File.Exists(pipelineEvent.SourceFile))
  545. throw new PipelineException("The source file '{0}' does not exist!", pipelineEvent.SourceFile);
  546. // Store the last write time of the source file
  547. // so we can detect if it has been changed.
  548. pipelineEvent.SourceTime = File.GetLastWriteTime(pipelineEvent.SourceFile);
  549. // Make sure we can find the importer and processor.
  550. var importer = CreateImporter(pipelineEvent.Importer);
  551. if (importer == null)
  552. throw new PipelineException("Failed to create importer '{0}'", pipelineEvent.Importer);
  553. // Try importing the content.
  554. object importedObject;
  555. if (RethrowExceptions)
  556. {
  557. try
  558. {
  559. var importContext = new PipelineImporterContext(this);
  560. importedObject = importer.Import(pipelineEvent.SourceFile, importContext);
  561. }
  562. catch (PipelineException)
  563. {
  564. throw;
  565. }
  566. catch (InvalidContentException)
  567. {
  568. throw;
  569. }
  570. catch (Exception inner)
  571. {
  572. throw new PipelineException(string.Format("Importer '{0}' had unexpected failure!", pipelineEvent.Importer), inner);
  573. }
  574. }
  575. else
  576. {
  577. var importContext = new PipelineImporterContext(this);
  578. importedObject = importer.Import(pipelineEvent.SourceFile, importContext);
  579. }
  580. // The pipelineEvent.Processor can be null or empty. In this case the
  581. // asset should be imported but not processed.
  582. if (string.IsNullOrEmpty(pipelineEvent.Processor))
  583. return importedObject;
  584. var processor = CreateProcessor(pipelineEvent.Processor, pipelineEvent.Parameters);
  585. if (processor == null)
  586. throw new PipelineException("Failed to create processor '{0}'", pipelineEvent.Processor);
  587. // Make sure the input type is valid.
  588. if (!processor.InputType.IsAssignableFrom(importedObject.GetType()))
  589. {
  590. throw new PipelineException(
  591. string.Format("The type '{0}' cannot be processed by {1} as a {2}!",
  592. importedObject.GetType().FullName,
  593. pipelineEvent.Processor,
  594. processor.InputType.FullName));
  595. }
  596. // Process the imported object.
  597. object processedObject;
  598. if (RethrowExceptions)
  599. {
  600. try
  601. {
  602. var processContext = new PipelineProcessorContext(this, pipelineEvent);
  603. processedObject = processor.Process(importedObject, processContext);
  604. }
  605. catch (PipelineException)
  606. {
  607. throw;
  608. }
  609. catch (InvalidContentException)
  610. {
  611. throw;
  612. }
  613. catch (Exception inner)
  614. {
  615. throw new PipelineException(string.Format("Processor '{0}' had unexpected failure!", pipelineEvent.Processor), inner);
  616. }
  617. }
  618. else
  619. {
  620. var processContext = new PipelineProcessorContext(this, pipelineEvent);
  621. processedObject = processor.Process(importedObject, processContext);
  622. }
  623. return processedObject;
  624. }
  625. public void CleanContent(string sourceFilepath, string outputFilepath = null)
  626. {
  627. // First try to load the event file.
  628. ResolveOutputFilepath(sourceFilepath, ref outputFilepath);
  629. string eventFilepath;
  630. var cachedEvent = LoadBuildEvent(outputFilepath, out eventFilepath);
  631. if (cachedEvent != null)
  632. {
  633. // Recursively clean additional (nested) assets.
  634. foreach (var asset in cachedEvent.BuildAsset)
  635. {
  636. string assetEventFilepath;
  637. var assetCachedEvent = LoadBuildEvent(asset, out assetEventFilepath);
  638. if (assetCachedEvent == null)
  639. {
  640. Logger.LogMessage("Cleaning {0}", asset);
  641. // Remove asset (.xnb file) from output folder.
  642. FileHelper.DeleteIfExists(asset);
  643. // Remove event file (.mgcontent file) from intermediate folder.
  644. FileHelper.DeleteIfExists(assetEventFilepath);
  645. continue;
  646. }
  647. CleanContent(string.Empty, asset);
  648. }
  649. // Remove related output files (non-XNB files) that were copied to the output folder.
  650. foreach (var asset in cachedEvent.BuildOutput)
  651. {
  652. Logger.LogMessage("Cleaning {0}", asset);
  653. FileHelper.DeleteIfExists(asset);
  654. }
  655. }
  656. Logger.LogMessage("Cleaning {0}", outputFilepath);
  657. // Remove asset (.xnb file) from output folder.
  658. FileHelper.DeleteIfExists(outputFilepath);
  659. // Remove event file (.mgcontent file) from intermediate folder.
  660. FileHelper.DeleteIfExists(eventFilepath);
  661. _pipelineBuildEvents.Remove(sourceFilepath);
  662. }
  663. private void WriteXnb(object content, PipelineBuildEvent pipelineEvent)
  664. {
  665. // Make sure the output directory exists.
  666. var outputFileDir = Path.GetDirectoryName(pipelineEvent.DestFile);
  667. Directory.CreateDirectory(outputFileDir);
  668. if (_compiler == null)
  669. _compiler = new ContentCompiler();
  670. // Write the XNB.
  671. using (var stream = new FileStream(pipelineEvent.DestFile, FileMode.Create, FileAccess.Write, FileShare.None))
  672. _compiler.Compile(stream, content, Platform, Profile, CompressContent, OutputDirectory, outputFileDir);
  673. // Store the last write time of the output XNB here
  674. // so we can verify it hasn't been tampered with.
  675. pipelineEvent.DestTime = File.GetLastWriteTime(pipelineEvent.DestFile);
  676. }
  677. /// <summary>
  678. /// Stores the pipeline build event (in memory) if no matching event is found.
  679. /// </summary>
  680. /// <param name="pipelineEvent">The pipeline build event.</param>
  681. private void TrackPipelineBuildEvent(PipelineBuildEvent pipelineEvent)
  682. {
  683. List<PipelineBuildEvent> pipelineBuildEvents;
  684. bool eventsFound = _pipelineBuildEvents.TryGetValue(pipelineEvent.SourceFile, out pipelineBuildEvents);
  685. if (!eventsFound)
  686. {
  687. pipelineBuildEvents = new List<PipelineBuildEvent>();
  688. _pipelineBuildEvents.Add(pipelineEvent.SourceFile, pipelineBuildEvents);
  689. }
  690. if (FindMatchingEvent(pipelineBuildEvents, pipelineEvent.DestFile, pipelineEvent.Importer, pipelineEvent.Processor, pipelineEvent.Parameters) == null)
  691. pipelineBuildEvents.Add(pipelineEvent);
  692. }
  693. /// <summary>
  694. /// Gets an automatic asset name, such as "AssetName_0".
  695. /// </summary>
  696. /// <param name="sourceFileName">The source file name.</param>
  697. /// <param name="importerName">The name of the content importer. Can be <see langword="null"/>.</param>
  698. /// <param name="processorName">The name of the content processor. Can be <see langword="null"/>.</param>
  699. /// <param name="processorParameters">The processor parameters. Can be <see langword="null"/>.</param>
  700. /// <returns>The asset name.</returns>
  701. public string GetAssetName(string sourceFileName, string importerName, string processorName, OpaqueDataDictionary processorParameters)
  702. {
  703. Debug.Assert(Path.IsPathRooted(sourceFileName), "Absolute path expected.");
  704. // Get source file name, which is used for lookup in _pipelineBuildEvents.
  705. sourceFileName = PathHelper.Normalize(sourceFileName);
  706. string relativeSourceFileName = PathHelper.GetRelativePath(ProjectDirectory, sourceFileName);
  707. List<PipelineBuildEvent> pipelineBuildEvents;
  708. if (_pipelineBuildEvents.TryGetValue(sourceFileName, out pipelineBuildEvents))
  709. {
  710. // This source file has already been build.
  711. // --> Compare pipeline build events.
  712. ResolveImporterAndProcessor(sourceFileName, ref importerName, ref processorName);
  713. var matchingEvent = FindMatchingEvent(pipelineBuildEvents, null, importerName, processorName, processorParameters);
  714. if (matchingEvent != null)
  715. {
  716. // Matching pipeline build event found.
  717. string existingName = matchingEvent.DestFile;
  718. existingName = PathHelper.GetRelativePath(OutputDirectory, existingName);
  719. existingName = existingName.Substring(0, existingName.Length - 4); // Remove ".xnb".
  720. return existingName;
  721. }
  722. Logger.LogMessage(string.Format("Warning: Asset {0} built multiple times with different settings.", relativeSourceFileName));
  723. }
  724. // No pipeline build event with matching settings found.
  725. // Get default asset name (= output file name relative to output folder without ".xnb").
  726. string directoryName = Path.GetDirectoryName(relativeSourceFileName);
  727. string fileName = Path.GetFileNameWithoutExtension(relativeSourceFileName);
  728. string assetName = Path.Combine(directoryName, fileName);
  729. assetName = PathHelper.Normalize(assetName);
  730. return AppendAssetNameSuffix(assetName);
  731. }
  732. /// <summary>
  733. /// Determines whether the specified list contains a matching pipeline build event.
  734. /// </summary>
  735. /// <param name="pipelineBuildEvents">The list of pipeline build events.</param>
  736. /// <param name="destFile">Absolute path to the output file. Can be <see langword="null"/>.</param>
  737. /// <param name="importerName">The name of the content importer. Can be <see langword="null"/>.</param>
  738. /// <param name="processorName">The name of the content processor. Can be <see langword="null"/>.</param>
  739. /// <param name="processorParameters">The processor parameters. Can be <see langword="null"/>.</param>
  740. /// <returns>
  741. /// The matching pipeline build event, or <see langword="null"/>.
  742. /// </returns>
  743. private PipelineBuildEvent FindMatchingEvent(List<PipelineBuildEvent> pipelineBuildEvents, string destFile, string importerName, string processorName, OpaqueDataDictionary processorParameters)
  744. {
  745. foreach (var existingBuildEvent in pipelineBuildEvents)
  746. {
  747. if ((destFile == null || existingBuildEvent.DestFile.Equals(destFile))
  748. && existingBuildEvent.Importer == importerName
  749. && existingBuildEvent.Processor == processorName)
  750. {
  751. var defaultValues = GetProcessorDefaultValues(processorName);
  752. if (PipelineBuildEvent.AreParametersEqual(existingBuildEvent.Parameters, processorParameters, defaultValues))
  753. {
  754. return existingBuildEvent;
  755. }
  756. }
  757. }
  758. return null;
  759. }
  760. /// <summary>
  761. /// Gets the asset name including a suffix, such as "_0". (The number is incremented
  762. /// automatically.
  763. /// </summary>
  764. /// <param name="baseAssetName">
  765. /// The asset name without suffix (relative to output folder).
  766. /// </param>
  767. /// <returns>The asset name with suffix.</returns>
  768. private string AppendAssetNameSuffix(string baseAssetName)
  769. {
  770. int index = 0;
  771. string assetName = baseAssetName + "_0";
  772. while (IsAssetNameUsed(assetName))
  773. {
  774. index++;
  775. assetName = baseAssetName + '_' + index;
  776. }
  777. return assetName;
  778. }
  779. /// <summary>
  780. /// Determines whether the specified asset name is already used.
  781. /// </summary>
  782. /// <param name="assetName">The asset name (relative to output folder).</param>
  783. /// <returns>
  784. /// <see langword="true"/> if the asset name is already used; otherwise,
  785. /// <see langword="false"/> if the name is available.
  786. /// </returns>
  787. private bool IsAssetNameUsed(string assetName)
  788. {
  789. string destFile = Path.Combine(OutputDirectory, assetName + ".xnb");
  790. return _pipelineBuildEvents.SelectMany(pair => pair.Value)
  791. .Select(pipelineEvent => pipelineEvent.DestFile)
  792. .Any(existingDestFile => destFile.Equals(existingDestFile, StringComparison.OrdinalIgnoreCase));
  793. }
  794. }
  795. }